diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 2e2ce673f4b8..f383901b7b31 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,33 +1,827 @@ -from pathlib import Path +import itertools import io +from pathlib import Path +import numpy as np import pytest from matplotlib import ft2font from matplotlib.testing.decorators import check_figures_equal import matplotlib.font_manager as fm +import matplotlib.path as mpath import matplotlib.pyplot as plt -def test_fallback_errors(): - file_name = fm.findfont('DejaVu Sans') +def test_ft2image_draw_rect_filled(): + width = 23 + height = 42 + for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]): + im = ft2font.FT2Image(width, height) + im.draw_rect_filled(x0, y0, x1, y1) + a = np.asarray(im) + assert a.dtype == np.uint8 + assert a.shape == (height, width) + if x0 == 100 or y0 == 200: + # All the out-of-bounds starts should get automatically clipped. + assert np.sum(a) == 0 + else: + # Otherwise, ends are clipped to the dimension, but are also _inclusive_. + filled = (min(x1 + 1, width) - x0) * (min(y1 + 1, height) - y0) + assert np.sum(a) == 255 * filled + + +def test_ft2font_dejavu_attrs(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'DejaVuSans' + assert font.family_name == 'DejaVu Sans' + assert font.style_name == 'Book' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 6241 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 5 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.KERNING | ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags + assert font.style_flags == 0 # Not italic or bold. + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -175 # Underline position. + assert font.underline_thickness == 90 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1901 # HHead Ascent. + assert font.descender == -483 # HHead Descent. + # Unconfirmed values. + assert font.height == 2384 + assert font.max_advance_width == 3838 + assert font.max_advance_height == 2384 + assert font.bbox == (-2090, -948, 3673, 2524) + + +def test_ft2font_cm_attrs(): + file = fm.findfont('cmtt10') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'Cmtt10' + assert font.family_name == 'cmtt10' + assert font.style_name == 'Regular' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 133 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 2 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags, font.face_flags + assert font.style_flags == 0 # Not italic or bold. + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 2048 # Em Size. + assert font.underline_position == -143 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 1276 # HHead Ascent. + assert font.descender == -489 # HHead Descent. + # Unconfirmed values. + assert font.height == 1765 + assert font.max_advance_width == 1536 + assert font.max_advance_height == 1765 + assert font.bbox == (-12, -477, 1280, 1430) + - with pytest.raises(TypeError, match="Fallback list must be a list"): +def test_ft2font_stix_bold_attrs(): + file = fm.findfont('STIXSizeTwoSym:bold') + font = ft2font.FT2Font(file) + assert font.fname == file + # Names extracted from FontForge: Font Information → PS Names tab. + assert font.postscript_name == 'STIXSizeTwoSym-Bold' + assert font.family_name == 'STIXSizeTwoSym' + assert font.style_name == 'Bold' + assert font.num_faces == 1 # Single TTF. + assert font.num_glyphs == 20 # From compact encoding view in FontForge. + assert font.num_fixed_sizes == 0 # All glyphs are scalable. + assert font.num_charmaps == 3 + # Other internal flags are set, so only check the ones we're allowed to test. + expected_flags = (ft2font.SCALABLE | ft2font.SFNT | ft2font.HORIZONTAL | + ft2font.GLYPH_NAMES) + assert (font.face_flags & expected_flags) == expected_flags, font.face_flags + assert font.style_flags == ft2font.BOLD + assert font.scalable + # From FontForge: Font Information → General tab → entry name below. + assert font.units_per_EM == 1000 # Em Size. + assert font.underline_position == -133 # Underline position. + assert font.underline_thickness == 20 # Underline height. + # From FontForge: Font Information → OS/2 tab → Metrics tab → entry name below. + assert font.ascender == 2095 # HHead Ascent. + assert font.descender == -404 # HHead Descent. + # Unconfirmed values. + assert font.height == 2499 + assert font.max_advance_width == 1130 + assert font.max_advance_height == 2499 + assert font.bbox == (4, -355, 1185, 2095) + + +def test_ft2font_invalid_args(tmp_path): + # filename argument. + with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): + ft2font.FT2Font(None) + file = tmp_path / 'invalid-font.ttf' + file.write_text('This is not a valid font file.') + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('rt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wt') as fd): + ft2font.FT2Font(fd) + with (pytest.raises(TypeError, match='to a font file or a binary-mode file object'), + file.open('wb') as fd): + ft2font.FT2Font(fd) + + file = fm.findfont('DejaVu Sans') + + # hinting_factor argument. + with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + ft2font.FT2Font(file, 1.3) + with pytest.raises(ValueError, match='hinting_factor must be greater than 0'): + ft2font.FT2Font(file, 0) + + with pytest.raises(TypeError, match='Fallback list must be a list'): # failing to be a list will fail before the 0 - ft2font.FT2Font(file_name, _fallback_list=(0,)) # type: ignore[arg-type] + ft2font.FT2Font(file, _fallback_list=(0,)) # type: ignore[arg-type] + with pytest.raises(TypeError, match='Fallback fonts must be FT2Font objects.'): + ft2font.FT2Font(file, _fallback_list=[0]) # type: ignore[list-item] - with pytest.raises( - TypeError, match="Fallback fonts must be FT2Font objects." - ): - ft2font.FT2Font(file_name, _fallback_list=[0]) # type: ignore[list-item] + # kerning_factor argument. + with pytest.raises(TypeError, match='cannot be interpreted as an integer'): + ft2font.FT2Font(file, _kerning_factor=1.3) -def test_ft2font_positive_hinting_factor(): - file_name = fm.findfont('DejaVu Sans') - with pytest.raises( - ValueError, match="hinting_factor must be greater than 0" - ): - ft2font.FT2Font(file_name, 0) +def test_ft2font_clear(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + font.set_text('ABabCDcd') + assert font.get_num_glyphs() == 8 + assert font.get_width_height() != (0, 0) + assert font.get_bitmap_offset() != (0, 0) + font.clear() + assert font.get_num_glyphs() == 0 + assert font.get_width_height() == (0, 0) + assert font.get_bitmap_offset() == (0, 0) + + +def test_ft2font_set_size(): + file = fm.findfont('DejaVu Sans') + # Default is 12pt @ 72 dpi. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1) + font.set_text('ABabCDcd') + orig = font.get_width_height() + font.set_size(24, 72) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + font.set_size(12, 144) + font.set_text('ABabCDcd') + assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) + + +def test_ft2font_charmaps(): + def enc(name): + # We don't expose the encoding enum from FreeType, but can generate it here. + # For DejaVu, there are 5 charmaps, but only 2 have enum entries in FreeType. + e = 0 + for x in name: + e <<= 8 + e += ord(x) + return e + + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + assert font.num_charmaps == 5 + + # Unicode. + font.select_charmap(enc('unic')) + unic = font.get_charmap() + font.set_charmap(0) # Unicode platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(1) # Unicode platform, modern subtable. + after = font.get_charmap() + assert unic == after + font.set_charmap(3) # Windows platform, Unicode BMP only. + after = font.get_charmap() + assert len(after) <= len(unic) + for chr, glyph in after.items(): + assert unic[chr] == glyph == font.get_char_index(chr) + font.set_charmap(4) # Windows platform, Unicode full repertoire, modern subtable. + after = font.get_charmap() + assert unic == after + + # This is just a random sample from FontForge. + glyph_names = { + 'non-existent-glyph-name': 0, + 'plusminus': 115, + 'Racute': 278, + 'perthousand': 2834, + 'seveneighths': 3057, + 'triagup': 3721, + 'uni01D3': 405, + 'uni0417': 939, + 'uni2A02': 4464, + 'u1D305': 5410, + 'u1F0A1': 5784, + } + for name, index in glyph_names.items(): + assert font.get_name_index(name) == index + if name == 'non-existent-glyph-name': + name = '.notdef' + # This doesn't always apply, but it does for DejaVu Sans. + assert font.get_glyph_name(index) == name + + # Apple Roman. + font.select_charmap(enc('armn')) + armn = font.get_charmap() + font.set_charmap(2) # Macintosh platform, Roman. + after = font.get_charmap() + assert armn == after + assert len(armn) <= 256 # 8-bit encoding. + # The first 128 characters of Apple Roman match ASCII, which also matches Unicode. + for o in range(1, 128): + if o not in armn or o not in unic: + continue + assert unic[o] == armn[o] + # Check a couple things outside the ASCII set that are different in each charset. + examples = [ + # (Unicode, Macintosh) + (0x2020, 0xA0), # Dagger. + (0x00B0, 0xA1), # Degree symbol. + (0x00A3, 0xA3), # Pound sign. + (0x00A7, 0xA4), # Section sign. + (0x00B6, 0xA6), # Pilcrow. + (0x221E, 0xB0), # Infinity symbol. + ] + for u, m in examples: + # Though the encoding is different, the glyph should be the same. + assert unic[u] == armn[m] + + +_expected_sfnt_names = { + 'DejaVu Sans': { + 0: 'Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.\n' + 'Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n' + 'DejaVu changes are in public domain\n', + 1: 'DejaVu Sans', + 2: 'Book', + 3: 'DejaVu Sans', + 4: 'DejaVu Sans', + 5: 'Version 2.35', + 6: 'DejaVuSans', + 8: 'DejaVu fonts team', + 11: 'http://dejavu.sourceforge.net', + 13: 'Fonts are (c) Bitstream (see below). ' + 'DejaVu changes are in public domain. ' + '''Glyphs imported from Arev fonts are (c) Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. ''' ''' + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the ''' ''' +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr.''', + 14: 'http://dejavu.sourceforge.net/wiki/index.php/License', + 16: 'DejaVu Sans', + 17: 'Book', + }, + 'cmtt10': { + 0: 'Copyright (C) 1994, Basil K. Malyshev. All Rights Reserved.' + '012BaKoMa Fonts Collection, Level-B.', + 1: 'cmtt10', + 2: 'Regular', + 3: 'FontMonger:cmtt10', + 4: 'cmtt10', + 5: '1.1/12-Nov-94', + 6: 'Cmtt10', + }, + 'STIXSizeTwoSym:bold': { + 0: 'Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the ' + 'American Chemical Society, the American Institute of Physics, the American ' + 'Mathematical Society, the American Physical Society, Elsevier, Inc., and ' + 'The Institute of Electrical and Electronic Engineers, Inc. Portions ' + 'copyright (c) 1998-2003 by MicroPress, Inc. Portions copyright (c) 1990 by ' + 'Elsevier, Inc. All rights reserved.', + 1: 'STIXSizeTwoSym', + 2: 'Bold', + 3: 'FontMaster:STIXSizeTwoSym-Bold:1.0.0', + 4: 'STIXSizeTwoSym-Bold', + 5: 'Version 1.0.0', + 6: 'STIXSizeTwoSym-Bold', + 7: 'STIX Fonts(TM) is a trademark of The Institute of Electrical and ' + 'Electronics Engineers, Inc.', + 9: 'MicroPress Inc., with final additions and corrections provided by Coen ' + 'Hoffman, Elsevier (retired)', + 10: 'Arie de Ruiter, who in 1995 was Head of Information Technology ' + 'Development at Elsevier Science, made a proposal to the STI Pub group, an ' + 'informal group of publishers consisting of representatives from the ' + 'American Chemical Society (ACS), American Institute of Physics (AIP), ' + 'American Mathematical Society (AMS), American Physical Society (APS), ' + 'Elsevier, and Institute of Electrical and Electronics Engineers (IEEE). ' + 'De Ruiter encouraged the members to consider development of a series of ' + 'Web fonts, which he proposed should be called the Scientific and ' + 'Technical Information eXchange, or STIX, Fonts. All STI Pub member ' + 'organizations enthusiastically endorsed this proposal, and the STI Pub ' + 'group agreed to embark on what has become a twelve-year project. The goal ' + 'of the project was to identify all alphabetic, symbolic, and other ' + 'special characters used in any facet of scientific publishing and to ' + 'create a set of Unicode-based fonts that would be distributed free to ' + 'every scientist, student, and other interested party worldwide. The fonts ' + 'would be consistent with the emerging Unicode standard, and would permit ' + 'universal representation of every character. With the release of the STIX ' + "fonts, de Ruiter's vision has been realized.", + 11: 'http://www.stixfonts.org', + 12: 'http://www.micropress-inc.com', + 13: 'As a condition for receiving these fonts at no charge, each person ' + 'downloading the fonts must agree to some simple license terms. The ' + 'license is based on the SIL Open Font License ' + '. The ' + 'SIL License is a free and open source license specifically designed for ' + 'fonts and related software. The basic terms are that the recipient will ' + 'not remove the copyright and trademark statements from the fonts and ' + 'that, if the person decides to create a derivative work based on the STIX ' + 'Fonts but incorporating some changes or enhancements, the derivative work ' + '("Modified Version") will carry a different name. The copyright and ' + 'trademark restrictions are part of the agreement between the STI Pub ' + 'companies and the typeface designer. The "renaming" restriction results ' + 'from the desire of the STI Pub companies to assure that the STIX Fonts ' + 'will continue to function in a predictable fashion for all that use them. ' + 'No copy of one or more of the individual Font typefaces that form the ' + 'STIX Fonts(TM) set may be sold by itself, but other than this one ' + 'restriction, licensees are free to sell the fonts either separately or as ' + 'part of a package that combines other software or fonts with this font ' + 'set.', + 14: 'http://www.stixfonts.org/user_license.html', + }, +} + + +@pytest.mark.parametrize('font_name, expected', _expected_sfnt_names.items(), + ids=_expected_sfnt_names.keys()) +def test_ft2font_get_sfnt(font_name, expected): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + sfnt = font.get_sfnt() + for name, value in expected.items(): + # Macintosh, Unicode 1.0, English, name. + assert sfnt.pop((1, 0, 0, name)) == value.encode('ascii') + # Microsoft, Unicode, English United States, name. + assert sfnt.pop((3, 1, 1033, name)) == value.encode('utf-16be') + assert sfnt == {} + + +_expected_sfnt_tables = { + 'DejaVu Sans': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (2, 22937), + 'checkSumAdjustment': -175678572, + 'magicNumber': 0x5F0F3CF5, + 'flags': 31, + 'unitsPerEm': 2048, + 'created': (0, 3514699492), 'modified': (0, 3514699492), + 'xMin': -2090, 'yMin': -948, 'xMax': 3673, 'yMax': 2524, + 'macStyle': 0, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 0, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 6241, + 'maxPoints': 852, 'maxComponentPoints': 104, 'maxTwilightPoints': 16, + 'maxContours': 43, 'maxComponentContours': 12, + 'maxZones': 2, + 'maxStorage': 153, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 1045, + 'maxSizeOfInstructions': 534, + 'maxComponentElements': 8, + 'maxComponentDepth': 4, + }, + 'OS/2': { + 'version': 1, + 'xAvgCharWidth': 1038, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 1331, 'ySubscriptYSize': 1433, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 286, + 'ySuperscriptXSize': 1331, 'ySuperscriptYSize': 1433, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 983, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04', + 'ulCharRange': (3875565311, 3523280383, 170156073, 67117068), + 'achVendID': b'PfEd', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 65535, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1901, 'descent': -483, 'lineGap': 0, + 'advanceWidthMax': 3838, + 'minLeftBearing': -2090, 'minRightBearing': -1455, + 'xMaxExtent': 3673, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 6226, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -130, 'underlineThickness': 90, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, + 'cmtt10': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 555110277, + 'magicNumber': 0x5F0F3CF5, + 'flags': 3, + 'unitsPerEm': 2048, + 'created': (0, 0), 'modified': (0, 0), + 'xMin': -12, 'yMin': -477, 'xMax': 1280, 'yMax': 1430, + 'macStyle': 0, + 'lowestRecPPEM': 6, + 'fontDirectionHint': 2, + 'indexToLocFormat': 1, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 133, + 'maxPoints': 94, 'maxComponentPoints': 0, 'maxTwilightPoints': 12, + 'maxContours': 5, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 6, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 200, + 'maxSizeOfInstructions': 100, + 'maxComponentElements': 4, + 'maxComponentDepth': 1, + }, + 'OS/2': { + 'version': 0, + 'xAvgCharWidth': 1075, + 'usWeightClass': 400, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 410, 'ySubscriptYSize': 369, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': -469, + 'ySuperscriptXSize': 410, 'ySuperscriptYSize': 369, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 1090, + 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, + 'sFamilyClass': 0, + 'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (0, 0, 0, 0), + 'achVendID': b'\x00\x00\x00\x00', + 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 9835, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 1276, 'descent': -489, 'lineGap': 0, + 'advanceWidthMax': 1536, + 'minLeftBearing': -12, 'minRightBearing': -29, + 'xMaxExtent': 1280, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 133, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -133, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': { + 'version': (1, 0), + 'fontNumber': 2147483648, + 'pitch': 1075, + 'xHeight': 905, + 'style': 0, + 'typeFamily': 0, + 'capHeight': 1276, + 'symbolSet': 0, + 'typeFace': b'cmtt10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'characterComplement': b'\xff\xff\xff\xff7\xff\xff\xfe', + 'strokeWeight': 0, + 'widthType': -5, + 'serifStyle': 64, + }, + }, + 'STIXSizeTwoSym:bold': { + 'invalid': None, + 'head': { + 'version': (1, 0), + 'fontRevision': (1, 0), + 'checkSumAdjustment': 1803408080, + 'magicNumber': 0x5F0F3CF5, + 'flags': 11, + 'unitsPerEm': 1000, + 'created': (0, 3359035786), 'modified': (0, 3359035786), + 'xMin': 4, 'yMin': -355, 'xMax': 1185, 'yMax': 2095, + 'macStyle': 1, + 'lowestRecPPEM': 8, + 'fontDirectionHint': 2, + 'indexToLocFormat': 0, + 'glyphDataFormat': 0, + }, + 'maxp': { + 'version': (1, 0), + 'numGlyphs': 20, + 'maxPoints': 37, 'maxComponentPoints': 0, 'maxTwilightPoints': 0, + 'maxContours': 1, 'maxComponentContours': 0, + 'maxZones': 2, + 'maxStorage': 1, + 'maxFunctionDefs': 64, + 'maxInstructionDefs': 0, + 'maxStackElements': 64, + 'maxSizeOfInstructions': 0, + 'maxComponentElements': 0, + 'maxComponentDepth': 0, + }, + 'OS/2': { + 'version': 2, + 'xAvgCharWidth': 598, + 'usWeightClass': 700, 'usWidthClass': 5, + 'fsType': 0, + 'ySubscriptXSize': 500, 'ySubscriptYSize': 500, + 'ySubscriptXOffset': 0, 'ySubscriptYOffset': 250, + 'ySuperscriptXSize': 500, 'ySuperscriptYSize': 500, + 'ySuperscriptXOffset': 0, 'ySuperscriptYOffset': 500, + 'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037, + 'sFamilyClass': 0, + 'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'ulCharRange': (3, 192, 0, 0), + 'achVendID': b'STIX', + 'fsSelection': 32, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 10217, + }, + 'hhea': { + 'version': (1, 0), + 'ascent': 2095, 'descent': -404, 'lineGap': 0, + 'advanceWidthMax': 1130, + 'minLeftBearing': 0, 'minRightBearing': -55, + 'xMaxExtent': 1185, + 'caretSlopeRise': 1, 'caretSlopeRun': 0, 'caretOffset': 0, + 'metricDataFormat': 0, 'numOfLongHorMetrics': 19, + }, + 'vhea': None, + 'post': { + 'format': (2, 0), + 'isFixedPitch': 0, 'italicAngle': (0, 0), + 'underlinePosition': -123, 'underlineThickness': 20, + 'minMemType42': 0, 'maxMemType42': 0, + 'minMemType1': 0, 'maxMemType1': 0, + }, + 'pclt': None, + }, +} + + +@pytest.mark.parametrize('font_name', _expected_sfnt_tables.keys()) +@pytest.mark.parametrize('header', _expected_sfnt_tables['DejaVu Sans'].keys()) +def test_ft2font_get_sfnt_table(font_name, header): + file = fm.findfont(font_name) + font = ft2font.FT2Font(file) + assert font.get_sfnt_table(header) == _expected_sfnt_tables[font_name][header] + + +@pytest.mark.parametrize('left, right, unscaled, unfitted, default', [ + # These are all the same class. + ('A', 'A', 57, 248, 256), ('A', 'À', 57, 248, 256), ('A', 'Á', 57, 248, 256), + ('A', 'Â', 57, 248, 256), ('A', 'Ã', 57, 248, 256), ('A', 'Ä', 57, 248, 256), + # And a few other random ones. + ('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024), + ('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512), +]) +def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): + file = fm.findfont('DejaVu Sans') + # With unscaled, these settings should produce exact values found in FontForge. + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(100, 100) + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_UNSCALED) == unscaled + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_UNFITTED) == unfitted + assert font.get_kerning(font.get_char_index(ord(left)), + font.get_char_index(ord(right)), + ft2font.KERNING_DEFAULT) == default + + +def test_ft2font_set_text(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + xys = font.set_text('') + np.testing.assert_array_equal(xys, np.empty((0, 2))) + assert font.get_width_height() == (0, 0) + assert font.get_num_glyphs() == 0 + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + # This string uses all the kerning pairs defined for test_ft2font_get_kerning. + xys = font.set_text('AADAT.XC-J') + np.testing.assert_array_equal( + xys, + [(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0), + (3200, 0), (3712, 0), (4032, 0)]) + assert font.get_width_height() == (4288, 768) + assert font.get_num_glyphs() == 10 + assert font.get_descent() == 192 + assert font.get_bitmap_offset() == (6, 0) + + +def test_ft2font_loading(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + for glyph in [font.load_char(ord('M')), + font.load_glyph(font.get_char_index(ord('M')))]: + assert glyph is not None + assert glyph.width == 576 + assert glyph.height == 576 + assert glyph.horiBearingX == 0 + assert glyph.horiBearingY == 576 + assert glyph.horiAdvance == 640 + assert glyph.linearHoriAdvance == 678528 + assert glyph.vertBearingX == -384 + assert glyph.vertBearingY == 64 + assert glyph.vertAdvance == 832 + assert glyph.bbox == (54, 0, 574, 576) + assert font.get_num_glyphs() == 2 # Both count as loaded. + # But neither has been placed anywhere. + assert font.get_width_height() == (0, 0) + assert font.get_descent() == 0 + assert font.get_bitmap_offset() == (0, 0) + + +def test_ft2font_drawing(): + expected_str = ( + ' ', + '11 11 ', + '11 11 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 1 1 1 ', + '1 11 1 ', + '1 11 1 ', + '1 1 ', + '1 1 ', + ' ', + ) + expected = np.array([ + [int(c) for c in line.replace(' ', '0')] for line in expected_str + ]) + expected *= 255 + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_text('M') + font.draw_glyphs_to_bitmap(antialiased=False) + image = font.get_image() + np.testing.assert_array_equal(image, expected) + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + glyph = font.load_char(ord('M')) + image = ft2font.FT2Image(expected.shape[1], expected.shape[0]) + font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) + np.testing.assert_array_equal(image, expected) + + +def test_ft2font_get_path(): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + vertices, codes = font.get_path() + assert vertices.shape == (0, 2) + assert codes.shape == (0, ) + font.load_char(ord('M')) + vertices, codes = font.get_path() + expected_vertices = np.array([ + (0.843750, 9.000000), (2.609375, 9.000000), # Top left. + (4.906250, 2.875000), # Top of midpoint. + (7.218750, 9.000000), (8.968750, 9.000000), # Top right. + (8.968750, 0.000000), (7.843750, 0.000000), # Bottom right. + (7.843750, 7.906250), # Point under top right. + (5.531250, 1.734375), (4.296875, 1.734375), # Bar under midpoint. + (1.984375, 7.906250), # Point under top left. + (1.984375, 0.000000), (0.843750, 0.000000), # Bottom left. + (0.843750, 9.000000), # Back to top left corner. + (0.000000, 0.000000), + ]) + np.testing.assert_array_equal(vertices, expected_vertices) + expected_codes = np.full(expected_vertices.shape[0], mpath.Path.LINETO, + dtype=mpath.Path.code_type) + expected_codes[0] = mpath.Path.MOVETO + expected_codes[-1] = mpath.Path.CLOSEPOLY + np.testing.assert_array_equal(codes, expected_codes) @pytest.mark.parametrize('family_name, file_name', diff --git a/src/ft2font.cpp b/src/ft2font.cpp index cb9952f3b374..34a602562735 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -304,8 +304,9 @@ FT2Font::~FT2Font() void FT2Font::clear() { - pen.x = 0; - pen.y = 0; + pen.x = pen.y = 0; + bbox.xMin = bbox.yMin = bbox.xMax = bbox.yMax = 0; + advance = 0; for (size_t i = 0; i < glyphs.size(); i++) { FT_Done_Glyph(glyphs[i]); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6d6e8722b63b..6a05680a474c 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -14,7 +14,10 @@ static PyObject *convert_xys_to_array(std::vector &xys) { npy_intp dims[] = {(npy_intp)xys.size() / 2, 2 }; if (dims[0] > 0) { - return PyArray_SimpleNewFromData(2, dims, NPY_DOUBLE, &xys[0]); + auto obj = PyArray_SimpleNew(2, dims, NPY_DOUBLE); + auto array = reinterpret_cast(obj); + memcpy(PyArray_DATA(array), xys.data(), PyArray_NBYTES(array)); + return obj; } else { return PyArray_SimpleNew(2, dims, NPY_DOUBLE); } @@ -631,7 +634,7 @@ const char *PyFT2Font_set_text__doc__ = "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" "You must call this before `.draw_glyphs_to_bitmap`.\n" - "A sequence of x,y positions is returned.\n"; + "A sequence of x,y positions in 26.6 subpixels is returned; divide by 64 for pixels.\n"; static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *kwds) { @@ -829,9 +832,9 @@ static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" "--\n\n" - "Draw a single glyph to the bitmap at pixel locations x, y\n" - "Note it is your responsibility to set up the bitmap manually\n" - "with ``set_bitmap_size(w, h)`` before this call is made.\n" + "Draw a single glyph to *image* at pixel locations *x*, *y*\n" + "Note it is your responsibility to create the image manually\n" + "with the correct size before this call is made.\n" "\n" "If you want automatic layout, use `.set_text` in combinations with\n" "`.draw_glyphs_to_bitmap`. This function is instead intended for people\n" @@ -1079,7 +1082,7 @@ static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) case 0: { char head_dict[] = "{s:(h,H), s:(h,H), s:l, s:l, s:H, s:H," - "s:(l,l), s:(l,l), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; + "s:(I,I), s:(I,I), s:h, s:h, s:h, s:h, s:H, s:H, s:h, s:h, s:h}"; TT_Header *t = (TT_Header *)table; return Py_BuildValue(head_dict, "version", FIXED_MAJOR(t->Table_Version), FIXED_MINOR(t->Table_Version), @@ -1088,8 +1091,14 @@ static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) "magicNumber", t->Magic_Number, "flags", t->Flags, "unitsPerEm", t->Units_Per_EM, - "created", t->Created[0], t->Created[1], - "modified", t->Modified[0], t->Modified[1], + // FreeType 2.6.1 defines these two timestamps as FT_Long, + // but they should be unsigned (fixed in 2.10.0): + // https://gitlab.freedesktop.org/freetype/freetype/-/commit/3e8ec291ffcfa03c8ecba1cdbfaa55f5577f5612 + // It's actually read from the file structure as two 32-bit + // values, so we need to cast down in size to prevent sign + // extension from producing huge 64-bit values. + "created", static_cast(t->Created[0]), static_cast(t->Created[1]), + "modified", static_cast(t->Modified[0]), static_cast(t->Modified[1]), "xMin", t->xMin, "yMin", t->yMin, "xMax", t->xMax,