diff --git a/.appveyor.yml b/.appveyor.yml index 13705adc99f9..83bdfd08ce79 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -60,6 +60,26 @@ install: - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 - micromamba activate mpl-dev +before_test: + - git config --global user.name 'Matplotlib' + - git config --global user.email 'nobody@matplotlib.org' + - git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + - git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + - ps: | + $conflicts = git diff --name-only --diff-filter=U ` + lib/matplotlib/tests/baseline_images ` + lib/mpl_toolkits/*/tests/baseline_images + if ($conflicts) { + git checkout --ours -- $conflicts + git add -- $conflicts + } + git status + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + - git commit -m "Preload test images from branch text-overhaul-figures" + test_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib diff --git a/.circleci/config.yml b/.circleci/config.yml index 94090783b764..85622ffa7013 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ commands: fonts-install: steps: - restore_cache: - key: fonts-4 + key: fonts-5 - run: name: Install custom fonts command: | @@ -80,7 +80,7 @@ commands: -O ~/.local/share/fonts/xkcd-Script.ttf || true fc-cache -f -v - save_cache: - key: fonts-4 + key: fonts-5 paths: - ~/.local/share/fonts/ @@ -125,7 +125,7 @@ commands: --no-build-isolation --editable .[dev] fi - save_cache: - key: build-deps-2 + key: build-deps-3 paths: - subprojects/packagecache diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index ae4b03a559fa..499a26e35f91 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -145,6 +145,10 @@ jobs: name: cibw-sdist path: dist/ + - name: Purge Strawberry Perl + if: startsWith(matrix.os, 'windows-') + run: Remove-Item -Recurse C:\Strawberry + - name: Build wheels for CPython 3.14 uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0298805eefa..f13a018ab07b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,6 +102,25 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Preload test images + run: | + git config --global user.name 'Matplotlib' + git config --global user.email 'nobody@matplotlib.org' + git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + conflicts=$(git diff --name-only --diff-filter=U \ + lib/matplotlib/tests/baseline_images \ + lib/mpl_toolkits/*/tests/baseline_images) + if [ -n "${conflicts}" ]; then + git checkout --ours -- ${conflicts} + git add -- ${conflicts} + fi + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + git commit -m 'Preload test images from branch text-overhaul-figures' + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -377,13 +396,14 @@ jobs: if [[ "${{ runner.os }}" != 'macOS' ]]; then LCOV_IGNORE_ERRORS=',' # do not ignore any lcov errors by default if [[ "${{ matrix.os }}" = ubuntu-24.04 || "${{ matrix.os }}" = ubuntu-24.04-arm ]]; then - # filter mismatch and unused-entity errors detected by lcov 2.x - LCOV_IGNORE_ERRORS='mismatch,unused' + # filter mismatch errors detected by lcov 2.x + LCOV_IGNORE_ERRORS='mismatch' fi lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ - --capture --directory . --output-file coverage.info + --capture --directory . --exclude $PWD/subprojects --exclude $PWD/build \ + --output-file coverage.info lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ - --output-file coverage.info --extract coverage.info $PWD/src/'*' $PWD/lib/'*' + --output-file coverage.info --extract coverage.info $PWD/src/'*' lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ --list coverage.info find . -name '*.gc*' -delete diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 11c73ce242a4..725daa918566 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -44,6 +44,25 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Preload test images + run: | + git config --global user.name 'Matplotlib' + git config --global user.email 'nobody@matplotlib.org' + git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + conflicts=$(git diff --name-only --diff-filter=U \ + lib/matplotlib/tests/baseline_images \ + lib/mpl_toolkits/*/tests/baseline_images) + if [ -n "${conflicts}" ]; then + git checkout --ours -- ${conflicts} + git add -- ${conflicts} + fi + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + git commit -m 'Preload test images from branch text-overhaul-figures' + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 name: Install Python with: @@ -54,6 +73,7 @@ jobs: env: CIBW_BUILD: "cp312-*" CIBW_PLATFORM: "pyodide" + CIBW_TEST_COMMAND: "true" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bba747d451b..9ddfcf6de835 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,13 +21,13 @@ repos: - id: check-docstring-first exclude: lib/matplotlib/typing.py # docstring used for attribute flagged by check - id: end-of-file-fixer - exclude_types: [svg] + exclude_types: [diff, svg] - id: mixed-line-ending - id: name-tests-test args: ["--pytest-test-first"] - id: no-commit-to-branch # Default is master and main. - id: trailing-whitespace - exclude_types: [svg] + exclude_types: [diff, svg] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: diff --git a/LICENSE/LICENSE_FREETYPE b/LICENSE/LICENSE_FREETYPE new file mode 100644 index 000000000000..8b9ce9e2e6e3 --- /dev/null +++ b/LICENSE/LICENSE_FREETYPE @@ -0,0 +1,46 @@ +FREETYPE LICENSES +----------------- + +The FreeType 2 font engine is copyrighted work and cannot be used +legally without a software license. In order to make this project +usable to a vast majority of developers, we distribute it under two +mutually exclusive open-source licenses. + +This means that *you* must choose *one* of the two licenses described +below, then obey all its terms and conditions when using FreeType 2 in +any of your projects or products. + + - The FreeType License, found in the file `docs/FTL.TXT`, which is + similar to the original BSD license *with* an advertising clause + that forces you to explicitly cite the FreeType project in your + product's documentation. All details are in the license file. + This license is suited to products which don't use the GNU General + Public License. + + Note that this license is compatible to the GNU General Public + License version 3, but not version 2. + + - The GNU General Public License version 2, found in + `docs/GPLv2.TXT` (any later version can be used also), for + programs which already use the GPL. Note that the FTL is + incompatible with GPLv2 due to its advertisement clause. + +The contributed BDF and PCF drivers come with a license similar to +that of the X Window System. It is compatible to the above two +licenses (see files `src/bdf/README` and `src/pcf/README`). The same +holds for the source code files `src/base/fthash.c` and +`include/freetype/internal/fthash.h`; they were part of the BDF driver +in earlier FreeType versions. + +The gzip module uses the zlib license (see `src/gzip/zlib.h`) which +too is compatible to the above two licenses. + +The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code +taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses +the 'Old MIT' license, compatible to the above two licenses. + +The MD5 checksum support (only used for debugging in development +builds) is in the public domain. + + +--- end of LICENSE.TXT --- diff --git a/LICENSE/LICENSE_HARFBUZZ b/LICENSE/LICENSE_HARFBUZZ new file mode 100644 index 000000000000..1dd917e9f2e7 --- /dev/null +++ b/LICENSE/LICENSE_HARFBUZZ @@ -0,0 +1,42 @@ +HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. +For parts of HarfBuzz that are licensed under different licenses see individual +files names COPYING in subdirectories where applicable. + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +For full copyright notices consult the individual files in the package. + + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build b/LICENSE/LICENSE_LIBRAQM similarity index 86% rename from subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build rename to LICENSE/LICENSE_LIBRAQM index ec288041f388..97e2489b7798 100644 --- a/subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build +++ b/LICENSE/LICENSE_LIBRAQM @@ -1,4 +1,7 @@ -Copyright (c) 2018 The Meson development team +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016-2023 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LICENSE/LICENSE_SHEENBIDI b/LICENSE/LICENSE_SHEENBIDI new file mode 100755 index 000000000000..d64569567334 --- /dev/null +++ b/LICENSE/LICENSE_SHEENBIDI @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 829a1c7b9005..cad8192de001 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,6 +68,25 @@ stages: architecture: 'x64' displayName: 'Use Python $(python.version)' + - bash: | + git config --global user.name 'Matplotlib' + git config --global user.email 'nobody@matplotlib.org' + git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + conflicts=$(git diff --name-only --diff-filter=U \ + lib/matplotlib/tests/baseline_images \ + lib/mpl_toolkits/*/tests/baseline_images) + if [ -n "${conflicts}" ]; then + git checkout --ours -- ${conflicts} + git add -- ${conflicts} + fi + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + git commit -m 'Preload test images from branch text-overhaul-figures' + displayName: Preload test images + - bash: | choco install ninja displayName: 'Install dependencies' diff --git a/ci/minver-requirements.txt b/ci/minver-requirements.txt index ee55f6c7b1bf..3b6aea9e7ca3 100644 --- a/ci/minver-requirements.txt +++ b/ci/minver-requirements.txt @@ -5,7 +5,7 @@ cycler==0.10 fonttools==4.22.0 importlib-resources==3.2.0 kiwisolver==1.3.2 -meson-python==0.13.1 +meson-python==0.13.2 meson==1.1.0 numpy==1.25.0 packaging==20.0 diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 359168608edf..0e199889cb07 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -6,6 +6,7 @@ matplotlib\._.* matplotlib\.rcsetup\._listify_validator matplotlib\.rcsetup\._validate_linestyle matplotlib\.ft2font\.Glyph +matplotlib\.ft2font\.LayoutItem matplotlib\.testing\.jpl_units\..* matplotlib\.sphinxext(\..*)? @@ -51,3 +52,7 @@ matplotlib\.inset\.InsetIndicator\.__getitem__ # only defined in stubs; not present at runtime matplotlib\.animation\.EventSourceProtocol + +# Avoid a regression in NewType handling for stubtest +# https://github.com/python/mypy/issues/19877 +matplotlib\.ft2font\.GlyphIndexType\.__init__ diff --git a/doc/api/next_api_changes/behavior/30318-ES.rst b/doc/api/next_api_changes/behavior/30318-ES.rst new file mode 100644 index 000000000000..805901dcb21d --- /dev/null +++ b/doc/api/next_api_changes/behavior/30318-ES.rst @@ -0,0 +1,9 @@ +FT2Font no longer sets a default size +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the interest of handling non-scalable fonts and reducing font initialization, the +`.FT2Font` constructor no longer sets a default size. Non-scalable fonts are sometimes +used for bitmap-backed emoji fonts. + +If metrics are important (i.e., if you are loading character glyphs, or setting a text +string), then explicitly call `.FT2Font.set_size` beforehand. diff --git a/doc/api/next_api_changes/behavior/30335-ES.rst b/doc/api/next_api_changes/behavior/30335-ES.rst new file mode 100644 index 000000000000..26b059401e19 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30335-ES.rst @@ -0,0 +1,15 @@ +``mathtext.VectorParse`` now includes glyph indices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a *path*-outputting `.MathTextParser`, in the return value of +`~.MathTextParser.parse`, (a `.VectorParse`), the *glyphs* field is now a list +containing tuples of: + +- font: `.FT2Font` +- fontsize: `float` +- character code: `int` +- glyph index: `int` +- x: `float` +- y: `float` + +Specifically, the glyph index was added after the character code. diff --git a/doc/api/next_api_changes/deprecations/30322-ES.rst b/doc/api/next_api_changes/deprecations/30322-ES.rst new file mode 100644 index 000000000000..b9c4964e58c8 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30322-ES.rst @@ -0,0 +1,7 @@ +Font kerning factor is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Due to internal changes to support complex text rendering, the kerning factor on fonts is +no longer used. Setting the ``text.kerning_factor`` rcParam (which existed only for +backwards-compatibility) to any value other than None is deprecated, and the rcParam will +be removed in the future. diff --git a/doc/api/next_api_changes/deprecations/30329-ES.rst b/doc/api/next_api_changes/deprecations/30329-ES.rst new file mode 100644 index 000000000000..8d5060c4821b --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30329-ES.rst @@ -0,0 +1,4 @@ +``font_manager.is_opentype_cff_font`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is no replacement. diff --git a/doc/api/next_api_changes/deprecations/30512-ES.rst b/doc/api/next_api_changes/deprecations/30512-ES.rst new file mode 100644 index 000000000000..f235964c5502 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30512-ES.rst @@ -0,0 +1,3 @@ +``PdfFile.multi_byte_charprocs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. diff --git a/doc/api/next_api_changes/development/30143-ES.rst b/doc/api/next_api_changes/development/30143-ES.rst new file mode 100644 index 000000000000..2d79ad6bbe9d --- /dev/null +++ b/doc/api/next_api_changes/development/30143-ES.rst @@ -0,0 +1,7 @@ +Glyph indices now typed distinctly from character codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, character codes and glyph indices were both typed as `int`, which means you +could mix and match them erroneously. While the character code can't be made a distinct +type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means +these can't be fully swapped. diff --git a/doc/missing-references.json b/doc/missing-references.json index d27d2fa067ce..7799e5b313da 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -3,6 +3,9 @@ "HashableList[_HT]": [ ":1" ], + "collections.abc.Sequence[tuple[str": [ + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1" + ], "matplotlib.axes._base._AxesBase": [ "doc/api/artist_api.rst:203" ], @@ -122,14 +125,15 @@ "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" ], "numpy.float64": [ - "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1", - "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1" ], "numpy.typing.NDArray": [ + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.get_image:1", "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1" ], "numpy.uint8": [ - ":1" + ":1", + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.get_image:1" ] }, "py:obj": { diff --git a/doc/project/license.rst b/doc/project/license.rst index aa41bcf8f75c..6a34eff4637d 100644 --- a/doc/project/license.rst +++ b/doc/project/license.rst @@ -71,6 +71,38 @@ Bundled software .. literalinclude:: ../../LICENSE/LICENSE_QT4_EDITOR :language: none +Rendering software +------------------ + +.. dropdown:: Agg + :class-container: sdd + + .. literalinclude:: ../../extern/agg24-svn/src/copying + :language: none + +.. dropdown:: FreeType + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_FREETYPE + :language: none + +.. dropdown:: Harfbuzz + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_HARFBUZZ + :language: none + +.. dropdown:: libraqm + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_LIBRAQM + :language: none + +.. dropdown:: SheenBidi + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_SHEENBIDI + :language: none .. _licenses-cmaps-styles: diff --git a/doc/release/next_whats_new/font_alt_family_names.rst b/doc/release/next_whats_new/font_alt_family_names.rst new file mode 100644 index 000000000000..11b67bf6d584 --- /dev/null +++ b/doc/release/next_whats_new/font_alt_family_names.rst @@ -0,0 +1,25 @@ +Fonts addressable by all their SFNT family names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fonts can now be selected by any of the family names they advertise in +the OpenType name table, not just the one FreeType reports as the primary +family name. + +Some fonts store different family names on different platforms or in +different name-table entries. For example, Ubuntu Light stores +``"Ubuntu"`` in the Macintosh-platform Name ID 1 slot (which FreeType +uses as the primary name) and ``"Ubuntu Light"`` in the Microsoft-platform +Name ID 1 slot. Previously only the FreeType-derived name was registered, +requiring an obscure weight-based workaround:: + + # Previously required + matplotlib.rcParams['font.family'] = 'Ubuntu' + matplotlib.rcParams['font.weight'] = 300 + +All name-table entries that describe a family — Name ID 1 on both +platforms, the Typographic Family (Name ID 16), and the WWS Family +(Name ID 21) — are now registered as separate entries in the +`~matplotlib.font_manager.FontManager`, so any of those names can be +used directly:: + + matplotlib.rcParams['font.family'] = 'Ubuntu Light' diff --git a/doc/release/next_whats_new/font_features.rst b/doc/release/next_whats_new/font_features.rst new file mode 100644 index 000000000000..022d36e1e21d --- /dev/null +++ b/doc/release/next_whats_new/font_features.rst @@ -0,0 +1,41 @@ +Specifying font feature tags +---------------------------- + +OpenType fonts may support feature tags that specify alternate glyph shapes or +substitutions to be made optionally. The text API now supports setting a list of feature +tags to be used with the associated font. Feature tags can be set/get with: + +- `matplotlib.text.Text.set_fontfeatures` / `matplotlib.text.Text.get_fontfeatures` +- Any API that creates a `.Text` object by passing the *fontfeatures* argument (e.g., + ``plt.xlabel(..., fontfeatures=...)``) + +Font feature strings are eventually passed to HarfBuzz, and so all `string formats +supported by hb_feature_from_string() +`__ are +supported. Note though that subranges are not explicitly supported and behaviour may +change in the future. + +For example, the default font ``DejaVu Sans`` enables Standard Ligatures (the ``'liga'`` +tag) by default, and also provides optional Discretionary Ligatures (the ``dlig`` tag.) +These may be toggled with ``+`` or ``-``. + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(7, 3)) + + fig.text(0.5, 0.85, 'Ligatures', fontsize=40, horizontalalignment='center') + + # Default has Standard Ligatures (liga). + fig.text(0, 0.6, 'Default: fi ffi fl st', fontsize=40) + + # Disable Standard Ligatures with -liga. + fig.text(0, 0.35, 'Disabled: fi ffi fl st', fontsize=40, + fontfeatures=['-liga']) + + # Enable Discretionary Ligatures with dlig. + fig.text(0, 0.1, 'Discretionary: fi ffi fl st', fontsize=40, + fontfeatures=['dlig']) + +Available font feature tags may be found at +https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/doc/release/next_whats_new/libraqm.rst b/doc/release/next_whats_new/libraqm.rst new file mode 100644 index 000000000000..8312f2f9432c --- /dev/null +++ b/doc/release/next_whats_new/libraqm.rst @@ -0,0 +1,42 @@ +Complex text layout with libraqm +-------------------------------- + +Text support has been extended to include complex text layout. This support includes: + +1. Languages that require advanced layout, such as Arabic or Hebrew. +2. Text that mixes left-to-right and right-to-left languages. + + .. plot:: + :show-source-link: False + + text = 'Here is some رَقْم in اَلْعَرَبِيَّةُ' + fig = plt.figure(figsize=(6, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + +3. Ligatures that combine several adjacent characters for improved legibility. + + .. plot:: + :show-source-link: False + + text = 'f\N{Hair Space}f\N{Hair Space}i \N{Rightwards Arrow} ffi' + fig = plt.figure(figsize=(3, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + +4. Combining multiple or double-width diacritics. + + .. plot:: + :show-source-link: False + + text = ( + 'a\N{Combining Circumflex Accent}\N{Combining Double Tilde}' + 'c\N{Combining Diaeresis}') + text = ' + '.join( + c if c in 'ac' else f'\N{Dotted Circle}{c}' + for c in text) + f' \N{Rightwards Arrow} {text}' + fig = plt.figure(figsize=(6, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center', + # Builtin DejaVu Sans doesn't support multiple diacritics. + family=['Noto Sans', 'DejaVu Sans']) + +Note, all advanced features require corresponding font support, and may require +additional fonts over the builtin DejaVu Sans. diff --git a/doc/release/next_whats_new/mathnormal.rst b/doc/release/next_whats_new/mathnormal.rst new file mode 100644 index 000000000000..7e4cd5d333fe --- /dev/null +++ b/doc/release/next_whats_new/mathnormal.rst @@ -0,0 +1,10 @@ +Mathtext distinguishes *italic* and *normal* font +------------------------------------------------- + +Matplotlib's lightweight TeX expression parser (``usetex=False``) now distinguishes between *italic* and *normal* math fonts to closer replicate the behaviour of LaTeX. +The normal math font is selected by default in math environment (unless the rcParam ``mathtext.default`` is overwritten) but can be explicitly set with the new ``\mathnormal`` command. Italic font is selected with ``\mathit``. +The main difference is that *italic* produces italic digits, whereas *normal* produces upright digits. Previously, it was not possible to typeset italic digits. +Note that ``normal`` now corresponds to what used to be ``it``, whereas ``it`` now renders all characters italic. +**Important**: In case the default mathematics font is overwritten by setting ``mathtext.default: it`` in ``matplotlibrc``, it must be either commented out or changed to ``mathtext.default: normal`` to preserve its behaviour. Otherwise, all alphanumeric characters, including digits, are rendered italic. + +One difference to traditional LaTeX is that LaTeX further distinguishes between *normal* (``\mathnormal``) and *default math*, where the default uses roman digits and normal uses oldstyle digits. This distinction is no longer present with modern LaTeX engines and unicode-math nor in Matplotlib. diff --git a/doc/release/next_whats_new/pdf_fonts.rst b/doc/release/next_whats_new/pdf_fonts.rst new file mode 100644 index 000000000000..4d8665386a72 --- /dev/null +++ b/doc/release/next_whats_new/pdf_fonts.rst @@ -0,0 +1,10 @@ +Improved font embedding in PDF +------------------------------ + +Both Type 3 and Type 42 fonts (see :ref:`fonts` for more details) are now +embedded into PDFs without limitation. Fonts may be split into multiple +embedded subsets in order to satisfy format limits. Additionally, a corrected +Unicode mapping is added for each. + +This means that *all* text should now be selectable and copyable in PDF viewers +that support doing so. diff --git a/doc/release/next_whats_new/tex_phantoms.rst b/doc/release/next_whats_new/tex_phantoms.rst new file mode 100644 index 000000000000..82d39d502fb5 --- /dev/null +++ b/doc/release/next_whats_new/tex_phantoms.rst @@ -0,0 +1,11 @@ +mathtext support for ``\phantom``, ``\llap``, ``\rlap`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +mathtext gained support for the TeX macros ``\phantom``, ``\llap``, and +``\rlap``. ``\phantom`` allows to occupy some space on the canvas as if +some text was being rendered, without actually rendering that text, whereas +``\llap`` and ``\rlap`` allows to render some text on the canvas while +pretending that it occupies no space. Altogether these macros allow some finer +control of text alignments. + +See https://www.tug.org/TUGboat/tb22-4/tb72perlS.pdf for a detailed description +of these macros. diff --git a/doc/release/next_whats_new/text_language.rst b/doc/release/next_whats_new/text_language.rst new file mode 100644 index 000000000000..1d4668587b43 --- /dev/null +++ b/doc/release/next_whats_new/text_language.rst @@ -0,0 +1,37 @@ +Specifying text language +------------------------ + +OpenType fonts may support language systems which can be used to select different +typographic conventions, e.g., localized variants of letters that share a single Unicode +code point, or different default font features. The text API now supports setting a +language to be used and may be set/get with: + +- `matplotlib.text.Text.set_language` / `matplotlib.text.Text.get_language` +- Any API that creates a `.Text` object by passing the *language* argument (e.g., + ``plt.xlabel(..., language=...)``) + +The language of the text must be in a format accepted by libraqm, namely `a BCP47 +language code `_. If None or +unset, then no particular language will be implied, and default font settings will be +used. + +For example, Matplotlib's default font ``DejaVu Sans`` supports language-specific glyphs +in the Serbian and Macedonian languages in the Cyrillic alphabet (vs Russian), +or the Sámi family of languages in the Latin alphabet (vs English). + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(7, 3)) + + char = '\U00000431' + fig.text(0.5, 0.8, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.6, f'Serbian: {char}', fontsize=40, language='sr') + fig.text(1, 0.6, f'Russian: {char}', fontsize=40, language='ru', + horizontalalignment='right') + + char = '\U0000014a' + fig.text(0.5, 0.3, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.1, f'Inari Sámi: {char}', fontsize=40, language='smn') + fig.text(1, 0.1, f'English: {char}', fontsize=40, language='en', + horizontalalignment='right') diff --git a/doc/release/next_whats_new/ttc_fonts.rst b/doc/release/next_whats_new/ttc_fonts.rst new file mode 100644 index 000000000000..b80b1186707b --- /dev/null +++ b/doc/release/next_whats_new/ttc_fonts.rst @@ -0,0 +1,18 @@ +Support for loading TrueType Collection fonts +--------------------------------------------- + +TrueType Collection fonts (commonly found as files with a ``.ttc`` extension) are now +supported. Namely, Matplotlib will include these file extensions in its scan for system +fonts, and will add all sub-fonts to its list of available fonts (i.e., the list from +`~.font_manager.get_font_names`). + +From most high-level API, this means you should be able to specify the name of any +sub-font in a collection just as you would any other font. Note that at this time, there +is no way to specify the entire collection with any sort of automated selection of the +internal sub-fonts. + +In the low-level API, to ensure backwards-compatibility while facilitating this new +support, a `.FontPath` instance (comprised of a font path and a sub-font index, with +behaviour similar to a `str`) may be passed to the font management API in place of a +simple `os.PathLike` path. Any font management API that previously returned a string path +now returns a `.FontPath` instance instead. diff --git a/doc/release/prev_whats_new/whats_new_3.2.0.rst b/doc/release/prev_whats_new/whats_new_3.2.0.rst index 4fcba4e5a0fc..ac37695989bc 100644 --- a/doc/release/prev_whats_new/whats_new_3.2.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.2.0.rst @@ -54,26 +54,15 @@ triangle meshes. Kerning adjustments now use correct values ------------------------------------------ -Due to an error in how kerning adjustments were applied, previous versions of -Matplotlib would under-correct kerning. This version will now correctly apply -kerning (for fonts supported by FreeType). To restore the old behavior (e.g., -for test images), you may set :rc:`text.kerning_factor` to 6 (instead of 0). -Other values have undefined behavior. - -.. plot:: - - import matplotlib.pyplot as plt - - # Use old kerning values: - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() - ax.text(0.0, 0.05, 'BRAVO\nAWKWARD\nVAT\nW.Test', fontsize=56) - ax.set_title('Before (text.kerning_factor = 6)') - -Note how the spacing between characters is uniform between their bounding boxes -(above). With corrected kerning (below), slanted characters (e.g., AV or VA) -will be spaced closer together, as well as various other character pairs, -depending on font support (e.g., T and e, or the period after the W). +Due to an error in how kerning adjustments were applied, previous versions of Matplotlib +would under-correct kerning. This version will now correctly apply kerning (for fonts +supported by FreeType). To restore the old behavior (e.g., for test images), you may set +the ``text.kerning_factor`` rcParam to 6 (instead of 0). Other values have undefined +behavior. + +With corrected kerning (below), slanted characters (e.g., AV or VA) will be spaced closer +together, as well as various other character pairs, depending on font support (e.g., T +and e, or the period after the W). .. plot:: diff --git a/extern/meson.build b/extern/meson.build index 5463183a9099..df6557a8e699 100644 --- a/extern/meson.build +++ b/extern/meson.build @@ -9,18 +9,59 @@ subdir('agg24-svn') if get_option('system-freetype') freetype_dep = dependency('freetype2', version: '>=9.11.3') else - # This is the version of FreeType to use when building a local version. It - # must match the value in `lib/matplotlib.__init__.py`. Also update the docs - # in `docs/devel/dependencies.rst`. Bump the cache key in - # `.circleci/config.yml` when changing requirements. - LOCAL_FREETYPE_VERSION = '2.6.1' - freetype_proj = subproject( - f'freetype-@LOCAL_FREETYPE_VERSION@', - default_options: ['default_library=static']) + 'freetype2', + default_options: [ + 'default_library=static', + 'brotli=disabled', + 'bzip2=disabled', + get_option('system-libraqm') ? 'harfbuzz=disabled' : 'harfbuzz=static', + 'mmap=auto', + 'png=disabled', + 'tests=disabled', + 'zlib=internal', + ]) freetype_dep = freetype_proj.get_variable('freetype_dep') endif +if get_option('system-libraqm') + libraqm_dep = dependency('raqm', version: '>=0.10.4') +else + subproject('harfbuzz', + default_options: [ + 'default_library=static', + 'benchmark=disabled', + 'cairo=disabled', + 'chafa=disabled', + 'coretext=disabled', + 'directwrite=disabled', + 'docs=disabled', + 'doc_tests=false', + 'fontations=disabled', + 'freetype=enabled', + 'gdi=disabled', + 'glib=disabled', + 'gobject=disabled', + 'harfrust=disabled', + 'icu=disabled', + 'introspection=disabled', + 'kbts=disabled', + 'tests=disabled', + 'utilities=disabled', + 'wasm=disabled', + ] + ) + subproject('sheenbidi', default_options: ['default_library=static']) + libraqm_proj = subproject('libraqm-0.10.4', + default_options: [ + 'default_library=static', + 'sheenbidi=true', + 'tests=false', + ] + ) + libraqm_dep = libraqm_proj.get_variable('libraqm_dep') +endif + if get_option('system-qhull') qhull_dep = dependency('qhull_r', version: '>=8.0.2', required: false) if not qhull_dep.found() diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index b4f3cc7d21df..a7bdc9d28347 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -779,6 +779,8 @@ def __setitem__(self, key, val): cval = valid_key(val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None + if key == "text.kerning_factor" and cval is not None: + _api.warn_deprecated("3.11", name="text.kerning_factor", obj_type="rcParam") self._set(key, cval) def __getitem__(self, key): @@ -1355,8 +1357,8 @@ def _val_or_rc(val, *rc_names): def _init_tests(): # The version of FreeType to install locally for running the tests. This must match - # the value in `meson.build`. - LOCAL_FREETYPE_VERSION = '2.6.1' + # the value in `subprojects/freetype2.wrap`. + LOCAL_FREETYPE_VERSION = '2.14.3' from matplotlib import ft2font if (ft2font.__freetype_version__ != LOCAL_FREETYPE_VERSION or diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py index 9094206c2d7c..af607b0374fc 100644 --- a/lib/matplotlib/_afm.py +++ b/lib/matplotlib/_afm.py @@ -27,17 +27,19 @@ being used. """ -from collections import namedtuple +import inspect import logging import re +from typing import BinaryIO, NamedTuple, TypedDict, cast from ._mathtext_data import uni2type1 +from .ft2font import CharacterCodeType, GlyphIndexType _log = logging.getLogger(__name__) -def _to_int(x): +def _to_int(x: bytes | str) -> int: # Some AFM files have floats where we are expecting ints -- there is # probably a better way to handle this (support floats, round rather than # truncate). But I don't know what the best approach is now and this @@ -46,7 +48,7 @@ def _to_int(x): return int(float(x)) -def _to_float(x): +def _to_float(x: bytes | str) -> float: # Some AFM files use "," instead of "." as decimal separator -- this # shouldn't be ambiguous (unless someone is wicked enough to use "," as # thousands separator...). @@ -57,27 +59,56 @@ def _to_float(x): return float(x.replace(',', '.')) -def _to_str(x): +def _to_str(x: bytes) -> str: return x.decode('utf8') -def _to_list_of_ints(s): +def _to_list_of_ints(s: bytes) -> list[int]: s = s.replace(b',', b' ') return [_to_int(val) for val in s.split()] -def _to_list_of_floats(s): +def _to_list_of_floats(s: bytes | str) -> list[float]: return [_to_float(val) for val in s.split()] -def _to_bool(s): +def _to_bool(s: bytes) -> bool: if s.lower().strip() in (b'false', b'0', b'no'): return False else: return True -def _parse_header(fh): +class FontMetricsHeader(TypedDict, total=False): + StartFontMetrics: float + FontName: str + FullName: str + FamilyName: str + Weight: str + ItalicAngle: float + IsFixedPitch: bool + FontBBox: list[int] + UnderlinePosition: float + UnderlineThickness: float + Version: str + # Some AFM files have non-ASCII characters (which are not allowed by the spec). + # Given that there is actually no public API to even access this field, just return + # it as straight bytes. + Notice: bytes + EncodingScheme: str + CapHeight: float # Is the second version a mistake, or + Capheight: float # do some AFM files contain 'Capheight'? -JKS + XHeight: float + Ascender: float + Descender: float + StdHW: float + StdVW: float + StartCharMetrics: int + CharacterSet: str + Characters: int + + +def _parse_header(fh: BinaryIO) -> FontMetricsHeader: """ Read the font metrics header (up to the char metrics). @@ -98,34 +129,15 @@ def _parse_header(fh): * '-168 -218 1000 898' -> [-168, -218, 1000, 898] """ header_converters = { - b'StartFontMetrics': _to_float, - b'FontName': _to_str, - b'FullName': _to_str, - b'FamilyName': _to_str, - b'Weight': _to_str, - b'ItalicAngle': _to_float, - b'IsFixedPitch': _to_bool, - b'FontBBox': _to_list_of_ints, - b'UnderlinePosition': _to_float, - b'UnderlineThickness': _to_float, - b'Version': _to_str, - # Some AFM files have non-ASCII characters (which are not allowed by - # the spec). Given that there is actually no public API to even access - # this field, just return it as straight bytes. - b'Notice': lambda x: x, - b'EncodingScheme': _to_str, - b'CapHeight': _to_float, # Is the second version a mistake, or - b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS - b'XHeight': _to_float, - b'Ascender': _to_float, - b'Descender': _to_float, - b'StdHW': _to_float, - b'StdVW': _to_float, - b'StartCharMetrics': _to_int, - b'CharacterSet': _to_str, - b'Characters': _to_int, + bool: _to_bool, + bytes: lambda x: x, + float: _to_float, + int: _to_int, + list[int]: _to_list_of_ints, + str: _to_str, } - d = {} + header_value_types = inspect.get_annotations(FontMetricsHeader) + d: FontMetricsHeader = {} first_line = True for line in fh: line = line.rstrip() @@ -147,14 +159,16 @@ def _parse_header(fh): else: val = b'' try: - converter = header_converters[key] - except KeyError: + key_str = _to_str(key) + value_type = header_value_types[key_str] + except (KeyError, UnicodeDecodeError): _log.error("Found an unknown keyword in AFM header (was %r)", key) continue try: - d[key] = converter(val) + converter = header_converters[value_type] + d[key_str] = converter(val) # type: ignore[literal-required] except ValueError: - _log.error('Value error parsing header in AFM: %s, %s', key, val) + _log.error('Value error parsing header in AFM: %r, %r', key, val) continue if key == b'StartCharMetrics': break @@ -163,8 +177,8 @@ def _parse_header(fh): return d -CharMetrics = namedtuple('CharMetrics', 'width, name, bbox') -CharMetrics.__doc__ = """ +class CharMetrics(NamedTuple): + """ Represents the character metrics of a single character. Notes @@ -172,13 +186,20 @@ def _parse_header(fh): The fields do currently only describe a subset of character metrics information defined in the AFM standard. """ + + width: float + name: str + bbox: tuple[int, int, int, int] + + CharMetrics.width.__doc__ = """The character width (WX).""" CharMetrics.name.__doc__ = """The character name (N).""" CharMetrics.bbox.__doc__ = """ The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" -def _parse_char_metrics(fh): +def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[CharacterCodeType, CharMetrics], + dict[str, CharMetrics]]: """ Parse the given filehandle for character metrics information. @@ -198,12 +219,12 @@ def _parse_char_metrics(fh): """ required_keys = {'C', 'WX', 'N', 'B'} - ascii_d = {} - name_d = {} - for line in fh: + ascii_d: dict[CharacterCodeType, CharMetrics] = {} + name_d: dict[str, CharMetrics] = {} + for bline in fh: # We are defensively letting values be utf8. The spec requires # ascii, but there are non-compliant fonts in circulation - line = _to_str(line.rstrip()) # Convert from byte-literal + line = _to_str(bline.rstrip()) if line.startswith('EndCharMetrics'): return ascii_d, name_d # Split the metric line into a dictionary, keyed by metric identifiers @@ -214,8 +235,9 @@ def _parse_char_metrics(fh): num = _to_int(vals['C']) wx = _to_float(vals['WX']) name = vals['N'] - bbox = _to_list_of_floats(vals['B']) - bbox = list(map(int, bbox)) + bbox = tuple(map(int, _to_list_of_floats(vals['B']))) + if len(bbox) != 4: + raise RuntimeError(f'Bad parse: bbox has {len(bbox)} elements, should be 4') metrics = CharMetrics(wx, name, bbox) # Workaround: If the character name is 'Euro', give it the # corresponding character code, according to WinAnsiEncoding (see PDF @@ -230,7 +252,7 @@ def _parse_char_metrics(fh): raise RuntimeError('Bad parse') -def _parse_kern_pairs(fh): +def _parse_kern_pairs(fh: BinaryIO) -> dict[tuple[str, str], float]: """ Return a kern pairs dictionary. @@ -242,12 +264,11 @@ def _parse_kern_pairs(fh): d['A', 'y'] = -50 """ - line = next(fh) if not line.startswith(b'StartKernPairs'): - raise RuntimeError('Bad start of kern pairs data: %s' % line) + raise RuntimeError(f'Bad start of kern pairs data: {line!r}') - d = {} + d: dict[tuple[str, str], float] = {} for line in fh: line = line.rstrip() if not line: @@ -257,21 +278,26 @@ def _parse_kern_pairs(fh): return d vals = line.split() if len(vals) != 4 or vals[0] != b'KPX': - raise RuntimeError('Bad kern pairs line: %s' % line) + raise RuntimeError(f'Bad kern pairs line: {line!r}') c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3]) d[(c1, c2)] = val raise RuntimeError('Bad kern pairs parse') -CompositePart = namedtuple('CompositePart', 'name, dx, dy') -CompositePart.__doc__ = """ - Represents the information on a composite element of a composite char.""" +class CompositePart(NamedTuple): + """Represents the information on a composite element of a composite char.""" + + name: bytes + dx: float + dy: float + + CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'.""" CompositePart.dx.__doc__ = """x-displacement of the part from the origin.""" CompositePart.dy.__doc__ = """y-displacement of the part from the origin.""" -def _parse_composites(fh): +def _parse_composites(fh: BinaryIO) -> dict[bytes, list[CompositePart]]: """ Parse the given filehandle for composites information. @@ -292,11 +318,11 @@ def _parse_composites(fh): will be represented as:: - composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0), - CompositePart(name='acute', dx=160, dy=170)] + composites[b'Aacute'] = [CompositePart(name=b'A', dx=0, dy=0), + CompositePart(name=b'acute', dx=160, dy=170)] """ - composites = {} + composites: dict[bytes, list[CompositePart]] = {} for line in fh: line = line.rstrip() if not line: @@ -306,6 +332,9 @@ def _parse_composites(fh): vals = line.split(b';') cc = vals[0].split() name, _num_parts = cc[1], _to_int(cc[2]) + if len(vals) != _num_parts + 2: # First element is 'CC', last is empty. + raise RuntimeError(f'Bad composites parse: expected {_num_parts} parts, ' + f'but got {len(vals) - 2}') pccParts = [] for s in vals[1:-1]: pcc = s.split() @@ -316,7 +345,8 @@ def _parse_composites(fh): raise RuntimeError('Bad composites parse') -def _parse_optional(fh): +def _parse_optional(fh: BinaryIO) -> tuple[dict[tuple[str, str], float], + dict[bytes, list[CompositePart]]]: """ Parse the optional fields for kern pair data and composites. @@ -329,44 +359,38 @@ def _parse_optional(fh): A dict containing composite information. May be empty. See `._parse_composites`. """ - optional = { - b'StartKernData': _parse_kern_pairs, - b'StartComposites': _parse_composites, - } - - d = {b'StartKernData': {}, - b'StartComposites': {}} + kern_data: dict[tuple[str, str], float] = {} + composites: dict[bytes, list[CompositePart]] = {} for line in fh: line = line.rstrip() if not line: continue - key = line.split()[0] + match line.split()[0]: + case b'StartKernData': + kern_data = _parse_kern_pairs(fh) + case b'StartComposites': + composites = _parse_composites(fh) - if key in optional: - d[key] = optional[key](fh) - - return d[b'StartKernData'], d[b'StartComposites'] + return kern_data, composites class AFM: - def __init__(self, fh): + def __init__(self, fh: BinaryIO): """Parse the AFM file in file object *fh*.""" self._header = _parse_header(fh) self._metrics, self._metrics_by_name = _parse_char_metrics(fh) self._kern, self._composite = _parse_optional(fh) - def get_str_bbox_and_descent(self, s): + def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]: """Return the string bounding box and the maximal descent.""" if not len(s): return 0, 0, 0, 0, 0 - total_width = 0 - namelast = None - miny = 1e9 + total_width = 0.0 + namelast = '' + miny = 1_000_000_000 maxy = 0 left = 0 - if not isinstance(s, str): - s = _to_str(s) for c in s: if c == '\n': continue @@ -386,50 +410,52 @@ def get_str_bbox_and_descent(self, s): return left, miny, total_width, maxy - miny, -miny - def get_glyph_name(self, glyph_ind): # For consistency with FT2Font. + def get_glyph_name(self, # For consistency with FT2Font. + glyph_ind: GlyphIndexType) -> str: """Get the name of the glyph, i.e., ord(';') is 'semicolon'.""" - return self._metrics[glyph_ind].name + return self._metrics[cast(CharacterCodeType, glyph_ind)].name - def get_char_index(self, c): # For consistency with FT2Font. + def get_char_index(self, # For consistency with FT2Font. + c: CharacterCodeType) -> GlyphIndexType: """ Return the glyph index corresponding to a character code point. Note, for AFM fonts, we treat the glyph index the same as the codepoint. """ - return c + return cast(GlyphIndexType, c) - def get_width_char(self, c): + def get_width_char(self, c: CharacterCodeType) -> float: """Get the width of the character code from the character metric WX field.""" return self._metrics[c].width - def get_width_from_char_name(self, name): + def get_width_from_char_name(self, name: str) -> float: """Get the width of the character from a type1 character name.""" return self._metrics_by_name[name].width - def get_kern_dist_from_name(self, name1, name2): + def get_kern_dist_from_name(self, name1: str, name2: str) -> float: """ Return the kerning pair distance (possibly 0) for chars *name1* and *name2*. """ return self._kern.get((name1, name2), 0) - def get_fontname(self): + def get_fontname(self) -> str: """Return the font name, e.g., 'Times-Roman'.""" - return self._header[b'FontName'] + return self._header['FontName'] @property - def postscript_name(self): # For consistency with FT2Font. + def postscript_name(self) -> str: # For consistency with FT2Font. return self.get_fontname() - def get_fullname(self): + def get_fullname(self) -> str: """Return the font full name, e.g., 'Times-Roman'.""" - name = self._header.get(b'FullName') + name = self._header.get('FullName') if name is None: # use FontName as a substitute - name = self._header[b'FontName'] + name = self._header['FontName'] return name - def get_familyname(self): + def get_familyname(self) -> str: """Return the font family name, e.g., 'Times'.""" - name = self._header.get(b'FamilyName') + name = self._header.get('FamilyName') if name is not None: return name @@ -440,26 +466,34 @@ def get_familyname(self): return re.sub(extras, '', name) @property - def family_name(self): # For consistency with FT2Font. + def family_name(self) -> str: # For consistency with FT2Font. """The font family name, e.g., 'Times'.""" return self.get_familyname() - def get_weight(self): + def get_weight(self) -> str: """Return the font weight, e.g., 'Bold' or 'Roman'.""" - return self._header[b'Weight'] + return self._header['Weight'] - def get_angle(self): + def get_angle(self) -> float: """Return the fontangle as float.""" - return self._header[b'ItalicAngle'] + return self._header['ItalicAngle'] + + def get_ascender(self) -> float: + """Return the ascent as float.""" + return self._header['Ascender'] - def get_capheight(self): + def get_capheight(self) -> float: """Return the cap height as float.""" - return self._header[b'CapHeight'] + return self._header['CapHeight'] + + def get_descender(self) -> float: + """Return the descent as float.""" + return self._header['Descender'] - def get_xheight(self): + def get_xheight(self) -> float: """Return the xheight as float.""" - return self._header[b'XHeight'] + return self._header['XHeight'] - def get_underline_thickness(self): + def get_underline_thickness(self) -> float: """Return the underline thickness as float.""" - return self._header[b'UnderlineThickness'] + return self._header['UnderlineThickness'] diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index b1d391a8be0b..21ec24d73286 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -38,7 +38,8 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import Glyph + from .ft2font import CharacterCodeType, Glyph, GlyphIndexType + ParserElement.enable_packrat() _log = logging.getLogger("matplotlib.mathtext") @@ -48,7 +49,7 @@ # FONTS -def get_unicode_index(symbol: str) -> int: # Publicly exported. +def get_unicode_index(symbol: str) -> CharacterCodeType: # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. @@ -86,7 +87,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, int, float, float]] + glyphs: list[tuple[FT2Font, float, CharacterCodeType, GlyphIndexType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -126,28 +127,30 @@ class Output: def __init__(self, box: Box): self.box = box self.glyphs: list[tuple[float, float, FontInfo]] = [] # (ox, oy, info) - self.rects: list[tuple[float, float, float, float]] = [] # (x1, y1, x2, y2) + self.rects: list[tuple[float, float, float, float]] = [] # (x, y, w, h) def to_vector(self) -> VectorParse: w, h, d = map( np.ceil, [self.box.width, self.box.height, self.box.depth]) - gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + gs = [(info.font, info.fontsize, info.num, info.glyph_index, + ox, h - oy + info.offset) for ox, oy, info in self.glyphs] - rs = [(x1, h - y2, x2 - x1, y2 - y1) - for x1, y1, x2, y2 in self.rects] + rs = [(bx, h - (by + bh), bw, bh) + for bx, by, bw, bh in self.rects] + # Output.rects has downwards ys, VectorParse.rects has upwards ys. return VectorParse(w, h + d, d, gs, rs) def to_raster(self, *, antialiased: bool) -> RasterParse: # Metrics y's and mathtext y's are oriented in opposite directions, # hence the switch between ymin and ymax. xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs], - *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + *[x for x, y, w, h in self.rects], 0]) - 1 ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs], - *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + *[y for x, y, w, h in self.rects], 0]) - 1 xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs], - *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + *[x + w for x, y, w, h in self.rects], 0]) + 1 ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs], - *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + *[y + h for x, y, w, h in self.rects], 0]) + 1 w = xmax - xmin h = ymax - ymin - self.box.depth d = ymax - ymin - self.box.height @@ -163,15 +166,15 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: info.font.draw_glyph_to_bitmap( image, int(ox), int(oy - info.metrics.iceberg), info.glyph, antialiased=antialiased) - for x1, y1, x2, y2 in shifted.rects: - height = max(int(y2 - y1) - 1, 0) + for x, y, bw, bh in shifted.rects: + height = max(int(bh) - 1, 0) if height == 0: - center = (y2 + y1) / 2 + center = y + bh / 2 y = int(center - (height + 1) / 2) else: - y = int(y1) - x1 = math.floor(x1) - x2 = math.ceil(x2) + y = int(y) + x1 = math.floor(x) + x2 = math.ceil(x + bw) image[y:y+height+1, x1:x2+1] = 0xff return RasterParse(0, 0, w, h + d, d, image) @@ -213,7 +216,8 @@ class FontInfo(NamedTuple): fontsize: float postscript_name: str metrics: FontMetrics - num: int + num: CharacterCodeType + glyph_index: GlyphIndexType glyph: Glyph offset: float @@ -265,8 +269,9 @@ def get_metrics(self, font: str, font_class: str, sym: str, fontsize: float, ---------- font : str One of the TeX font names: "tt", "it", "rm", "cal", "sf", "bf", - "default", "regular", "bb", "frak", "scr". "default" and "regular" - are synonyms and use the non-math font. + "default", "regular", "normal", "bb", "frak", "scr". "default" + and "regular" are synonyms and use the non-math font. + "normal" denotes the normal math font. font_class : str One of the TeX font names (as for *font*), but **not** "bb", "frak", or "scr". This is used to combine two font classes. The @@ -296,11 +301,23 @@ def render_glyph(self, output: Output, ox: float, oy: float, font: str, output.glyphs.append((ox, oy, info)) def render_rect_filled(self, output: Output, - x1: float, y1: float, x2: float, y2: float) -> None: + x: float, y: float, w: float, h: float) -> None: + """ + Draw a filled rectangle at (*x*, *y*) with size (*w*, *h*). """ - Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*). + output.rects.append((x, y, w, h)) + + def get_axis_height(self, font: str, fontsize: float, dpi: float) -> float: """ - output.rects.append((x1, y1, x2, y2)) + Get the axis height for the given *font* and *fontsize*. + """ + raise NotImplementedError() + + def get_quad(self, font: str, fontsize: float, dpi: float) -> float: + """ + Get the size of a quad for the given *font* and *fontsize*. + """ + raise NotImplementedError() def get_xheight(self, font: str, fontsize: float, dpi: float) -> float: """ @@ -325,6 +342,9 @@ def get_sized_alternatives_for_symbol(self, fontname: str, """ return [(fontname, sym)] + def get_font_constants(self) -> type[FontConstantsBase]: + return FontConstantsBase + class TruetypeFonts(Fonts, metaclass=abc.ABCMeta): """ @@ -361,7 +381,7 @@ def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float, return 0. def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: raise NotImplementedError # The return value of _get_info is cached per-instance. @@ -369,7 +389,8 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, dpi: float) -> FontInfo: font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char(num, flags=self.load_glyph_flags) + glyph_index = font.get_char_index(num) + glyph = font.load_glyph(glyph_index, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) @@ -392,21 +413,42 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, postscript_name=font.postscript_name, metrics=metrics, num=num, + glyph_index=glyph_index, glyph=glyph, offset=offset ) + def get_axis_height(self, fontname: str, fontsize: float, dpi: float) -> float: + consts = self.get_font_constants() + if consts.axis_height is not None: + return consts.axis_height * fontsize * dpi / 72 + else: + # The fraction line (if present) must be aligned with the minus sign. + # Therefore, the height of the latter from the baseline is the axis height. + metrics = self.get_metrics( + fontname, mpl.rcParams['mathtext.default'], '\u2212', fontsize, dpi) + return (metrics.ymax + metrics.ymin) / 2 + + def get_quad(self, fontname: str, fontsize: float, dpi: float) -> float: + consts = self.get_font_constants() + if consts.quad is not None: + return consts.quad * fontsize * dpi / 72 + else: + # With no other option, we measure the size of an 'm'. + metrics = self.get_metrics( + fontname, mpl.rcParams['mathtext.default'], 'm', fontsize, dpi) + return metrics.advance + def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: - font = self._get_font(fontname) - font.set_size(fontsize, dpi) - pclt = font.get_sfnt_table('pclt') - if pclt is None: - # Some fonts don't store the xHeight, so we do a poor man's xHeight + consts = self.get_font_constants() + if consts.x_height is not None: + return consts.x_height * fontsize * dpi / 72 + else: + # Some fonts report the wrong x-height, while some don't store it, so + # we do a poor man's x-height. metrics = self.get_metrics( fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi) return metrics.iceberg - x_height = (pclt['xHeight'] / 64) * (fontsize / 12) * (dpi / 100) - return x_height def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: # This function used to grab underline thickness from the font @@ -421,7 +463,8 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64 + return font.get_kerning(info1.glyph_index, info2.glyph_index, + Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) @@ -434,10 +477,11 @@ class BakomaFonts(TruetypeFonts): its own proprietary 8-bit encoding. """ _fontmap = { + 'normal': 'cmmi10', 'cal': 'cmsy10', 'rm': 'cmr10', 'tt': 'cmtt10', - 'it': 'cmmi10', + 'it': 'cmti10', 'bf': 'cmb10', 'sf': 'cmss10', 'ex': 'cmex10', @@ -455,14 +499,20 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: font = None + if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] - slanted = (basename == "cmmi10") or sym in self._slanted_symbols + slanted = (basename in ("cmmi10", "cmti10")) or sym in self._slanted_symbols font = self._get_font(basename) elif len(sym) == 1: - slanted = (fontname == "it") + slanted = (fontname in ("it", "normal")) + if fontname == "normal" and sym.isdigit(): + # use digits from cmr (roman alphabet) instead of cmm (math alphabet), + # same as LaTeX does. + fontname = "rm" + slanted = False font = self._get_font(fontname) if font is not None: num = ord(sym) @@ -471,66 +521,49 @@ def _get_glyph(self, fontname: str, font_class: str, else: return self._stix_fallback._get_glyph(fontname, font_class, sym) - # The Bakoma fonts contain many pre-sized alternatives for the - # delimiters. The AutoSizedChar class will use these alternatives - # and select the best (closest sized) glyph. + # The Bakoma fonts contain many pre-sized alternatives for the delimiters. The + # Auto(Height|Width)Char classes will use these alternatives and select the best + # (closest sized) glyph. + _latex_sizes = ('big', 'Big', 'bigg', 'Bigg') _size_alternatives = { - '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'), - ('ex', '\xb5'), ('ex', '\xc3')], - ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'), - ('ex', '\xb6'), ('ex', '\x21')], - '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'), - ('ex', '\xbd'), ('ex', '\x28')], - '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'), - ('ex', '\xbe'), ('ex', '\x29')], - # The fourth size of '[' is mysteriously missing from the BaKoMa - # font, so I've omitted it for both '[' and ']' - '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'), - ('ex', '\x22')], - ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'), - ('ex', '\x23')], - r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'), - ('ex', '\xb9'), ('ex', '\x24')], - r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'), - ('ex', '\xba'), ('ex', '\x25')], - r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'), - ('ex', '\xbb'), ('ex', '\x26')], - r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'), - ('ex', '\xbc'), ('ex', '\x27')], - r'\langle': [('ex', '\xad'), ('ex', '\x44'), - ('ex', '\xbf'), ('ex', '\x2a')], - r'\rangle': [('ex', '\xae'), ('ex', '\x45'), - ('ex', '\xc0'), ('ex', '\x2b')], - r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'), - ('ex', '\x72'), ('ex', '\x73')], - r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'), - ('ex', '\xc2'), ('ex', '\x2d')], - r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'), - ('ex', '\xcb'), ('ex', '\x2c')], - r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'), - ('ex', '\x64')], - r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'), - ('ex', '\x67')], - r'<': [('cal', 'h'), ('ex', 'D')], - r'>': [('cal', 'i'), ('ex', 'E')] - } + '(': [('rm', '('), *[('ex', fr'\__parenleft{s}__') for s in _latex_sizes]], + ')': [('rm', ')'), *[('ex', fr'\__parenright{s}__') for s in _latex_sizes]], + '{': [('ex', fr'\__braceleft{s}__') for s in _latex_sizes], + '}': [('ex', fr'\__braceright{s}__') for s in _latex_sizes], + '[': [('rm', '['), *[('ex', fr'\__bracketleft{s}__') for s in _latex_sizes]], + ']': [('rm', ']'), *[('ex', fr'\__bracketright{s}__') for s in _latex_sizes]], + '<': [('cal', r'\__angbracketleft__'), + *[('ex', fr'\__angbracketleft{s}__') for s in _latex_sizes]], + '>': [('cal', r'\__angbracketright__'), + *[('ex', fr'\__angbracketright{s}__') for s in _latex_sizes]], + r'\lfloor': [('ex', fr'\__floorleft{s}__') for s in _latex_sizes], + r'\rfloor': [('ex', fr'\__floorright{s}__') for s in _latex_sizes], + r'\lceil': [('ex', fr'\__ceilingleft{s}__') for s in _latex_sizes], + r'\rceil': [('ex', fr'\__ceilingright{s}__') for s in _latex_sizes], + r'\__sqrt__': [('ex', fr'\__radical{s}__') for s in _latex_sizes], + r'\backslash': [('ex', fr'\__backslash{s}__') for s in _latex_sizes], + r'/': [('rm', '/'), *[('ex', fr'\__slash{s}__') for s in _latex_sizes]], + r'\widehat': [('rm', '\x5e'), ('ex', r'\__hatwide__'), ('ex', r'\__hatwider__'), + ('ex', r'\__hatwidest__')], + r'\widetilde': [('rm', '\x7e'), ('ex', r'\__tildewide__'), + ('ex', r'\__tildewider__'), ('ex', r'\__tildewidest__')], + } - for alias, target in [(r'\leftparen', '('), - (r'\rightparen', ')'), - (r'\leftbrace', '{'), - (r'\rightbrace', '}'), - (r'\leftbracket', '['), - (r'\rightbracket', ']'), - (r'\{', '{'), - (r'\}', '}'), - (r'\[', '['), - (r'\]', ']')]: + for alias, target in [(r'\leftparen', '('), (r'\rightparen', ')'), + (r'\leftbrace', '{'), (r'\rightbrace', '}'), + (r'\leftbracket', '['), (r'\rightbracket', ']'), + (r'\langle', '<'), (r'\rangle', '>'), + (r'\{', '{'), (r'\}', '}'), + (r'\[', '['), (r'\]', ']')]: _size_alternatives[alias] = _size_alternatives[target] def get_sized_alternatives_for_symbol(self, fontname: str, sym: str) -> list[tuple[str, str]]: return self._size_alternatives.get(sym, [(fontname, sym)]) + def get_font_constants(self) -> type[FontConstantsBase]: + return ComputerModernFontConstants + class UnicodeFonts(TruetypeFonts): """ @@ -547,7 +580,7 @@ class UnicodeFonts(TruetypeFonts): # Some glyphs are not present in the `cmr10` font, and must be brought in # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at # which they are found in `cmsy10`. - _cmr10_substitutions = { + _cmr10_substitutions: dict[CharacterCodeType, CharacterCodeType] = { 0x00D7: 0x00A3, # Multiplication sign. 0x2212: 0x00A1, # Minus sign. } @@ -591,11 +624,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: return fontname, uniindex def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: try: uniindex = get_unicode_index(sym) found_symbol = True @@ -604,19 +637,21 @@ def _get_glyph(self, fontname: str, font_class: str, found_symbol = False _log.warning("No TeX to Unicode mapping for %a.", sym) - fontname, uniindex = self._map_virtual_font( - fontname, font_class, uniindex) + fontname, uniindex = self._map_virtual_font(fontname, font_class, uniindex) new_fontname = fontname # Only characters in the "Letter" class should be italicized in 'it' # mode. Greek capital letters should be Roman. if found_symbol: - if fontname == 'it' and uniindex < 0x10000: + if fontname == 'normal' and uniindex < 0x10000: + # normal mathematics font char = chr(uniindex) if (unicodedata.category(char)[0] != "L" or unicodedata.name(char).startswith("GREEK CAPITAL")): new_fontname = 'rm' + else: + new_fontname = 'it' slanted = (new_fontname == 'it') or sym in self._slanted_symbols found_symbol = False @@ -633,7 +668,7 @@ def _get_glyph(self, fontname: str, font_class: str, if not found_symbol: if self._fallback_font: - if (fontname in ('it', 'regular') + if (fontname in ('it', 'regular', 'normal') and isinstance(self._fallback_font, StixFonts)): fontname = 'rm' @@ -645,7 +680,7 @@ def _get_glyph(self, fontname: str, font_class: str, return g else: - if (fontname in ('it', 'regular') + if (fontname in ('it', 'regular', 'normal') and isinstance(self, StixFonts)): return self._get_glyph('rm', font_class, sym) _log.warning("Font %r does not have a glyph for %a [U+%x], " @@ -689,7 +724,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: # Override prime symbol to use Bakoma. if sym == r'\prime': return self.bakoma._get_glyph(fontname, font_class, sym) @@ -722,6 +757,9 @@ class DejaVuSerifFonts(DejaVuFonts): '0': 'DejaVu Serif', } + def get_font_constants(self) -> type[FontConstantsBase]: + return DejaVuSerifFontConstants + class DejaVuSansFonts(DejaVuFonts): """ @@ -740,6 +778,9 @@ class DejaVuSansFonts(DejaVuFonts): '0': 'DejaVu Sans', } + def get_font_constants(self) -> type[FontConstantsBase]: + return DejaVuSansFontConstants + class StixFonts(UnicodeFonts): """ @@ -779,7 +820,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: # Handle these "fonts" that are actually embedded in # other fonts. font_mapping = stix_virtual_fonts.get(fontname) @@ -823,7 +864,7 @@ def _map_virtual_font(self, fontname: str, font_class: str, fontname = mpl.rcParams['mathtext.default'] # Fix some incorrect glyphs. - if fontname in ('rm', 'it'): + if fontname in ('rm', 'it', 'normal'): uniindex = stix_glyph_fixes.get(uniindex, uniindex) # Handle private use area glyphs @@ -853,6 +894,12 @@ def get_sized_alternatives_for_symbol(self, fontname: str, alternatives = alternatives[:-1] return alternatives + def get_font_constants(self) -> type[FontConstantsBase]: + if self._sans: + return STIXSansFontConstants + else: + return STIXFontConstants + class StixSansFonts(StixFonts): """ @@ -902,7 +949,10 @@ class FontConstantsBase: # Percentage of x-height of additional horiz. space after sub/superscripts script_space: T.ClassVar[float] = 0.05 - # Percentage of x-height that sub/superscripts drop below the baseline + # Percentage of x-height that superscripts drop below the top of large box + supdrop: T.ClassVar[float] = 0.4 + + # Percentage of x-height that subscripts drop below the bottom of large box subdrop: T.ClassVar[float] = 0.4 # Percentage of x-height that superscripts are raised from the baseline @@ -928,76 +978,138 @@ class FontConstantsBase: # integrals delta_integral: T.ClassVar[float] = 0.1 + # Percentage of x-height the numerator is shifted up in display style. + num1: T.ClassVar[float] = 1.4 + + # Percentage of x-height the numerator is shifted up in text, script and + # scriptscript styles if there is a fraction line. + num2: T.ClassVar[float] = 1.5 + + # Percentage of x-height the numerator is shifted up in text, script and + # scriptscript styles if there is no fraction line. + num3: T.ClassVar[float] = 1.3 + + # Percentage of x-height the denominator is shifted down in display style. + denom1: T.ClassVar[float] = 1.3 + + # Percentage of x-height the denominator is shifted down in text, script + # and scriptscript styles. + denom2: T.ClassVar[float] = 1.1 + + # The height of a horizontal reference line used for positioning elements in a + # formula, similar to a baseline, as a multiple of design size. + axis_height: T.ClassVar[float | None] = None + + # The size of a quad space in LaTeX, as a multiple of design size. + quad: T.ClassVar[float | None] = None + + # The size of x-height in font design units (i.e., divided by units-per-em). If not + # provided, then this will be measured from the font itself. + x_height: T.ClassVar[float | None] = None + class ComputerModernFontConstants(FontConstantsBase): - script_space = 0.075 - subdrop = 0.2 - sup1 = 0.45 - sub1 = 0.2 - sub2 = 0.3 - delta = 0.075 + # Previously, the x-height of Computer Modern was obtained from the font + # table. However, that x-height was greater than the the actual (rendered) + # x-height by a factor of 1.771484375 (at font size 12, DPI 100 and hinting + # type 32). Now that we're using the rendered x-height, some font constants + # have been increased by the same factor to compensate. + script_space = 0.132861328125 + delta = 0.132861328125 delta_slanted = 0.3 delta_integral = 0.3 + _x_height = 451470 + # These all come from the cmsy10.tfm metrics, divided by the design xheight from + # there, since we multiply these values by the scaled xheight later. + supdrop = 404864 / _x_height + subdrop = 52429 / _x_height + sup1 = 432949 / _x_height + sub1 = 157286 / _x_height + sub2 = 259226 / _x_height + num1 = 709370 / _x_height + num2 = 412858 / _x_height + num3 = 465286 / _x_height + denom1 = 719272 / _x_height + denom2 = 361592 / _x_height + # These come from the cmsy10.tfm metrics, scaled so they are in multiples of design + # size. + axis_height = 262144 / 2**20 + quad = 1048579 / 2**20 + x_height = _x_height / 2**20 class STIXFontConstants(FontConstantsBase): script_space = 0.1 - sup1 = 0.8 - sub2 = 0.6 delta = 0.05 delta_slanted = 0.3 delta_integral = 0.3 - - -class STIXSansFontConstants(FontConstantsBase): + _x_height = 450 + x_height = _x_height / 1000 + # These values are extracted from the TeX table of STIXGeneral.ttf using FontForge, + # and then divided by design xheight, since we multiply these values by the scaled + # xheight later. + supdrop = 386 / _x_height + subdrop = 50.0002 / _x_height + sup1 = 413 / _x_height + sub1 = 150 / _x_height + sub2 = 309 / _x_height + num1 = 747 / _x_height + num2 = 424 / _x_height + num3 = 474 / _x_height + denom1 = 756 / _x_height + denom2 = 375 / _x_height + # These come from the same TeX table, scaled by Em size so they are in multiples of + # design size. + axis_height = 250 / 1000 + quad = 1000 / 1000 + + +class STIXSansFontConstants(STIXFontConstants): script_space = 0.05 - sup1 = 0.8 delta_slanted = 0.6 delta_integral = 0.3 class DejaVuSerifFontConstants(FontConstantsBase): - pass + _x_height = 1063 + x_height = _x_height / 2048 + # These values are extracted from the TeX table of DejaVuSerif.ttf using FontForge, + # and then divided by design xheight, since we multiply these values by the scaled + # xheight later. + supdrop = 790.527 / _x_height + subdrop = 102.4 / _x_height + sup1 = 845.824 / _x_height + sub1 = 307.199 / _x_height + sub2 = 632.832 / _x_height + num1 = 1529.86 / _x_height + num2 = 868.352 / _x_height + num3 = 970.752 / _x_height + denom1 = 1548.29 / _x_height + denom2 = 768 / _x_height + # These come from the same TeX table, scaled by Em size so they are in multiples of + # design size. + axis_height = 512 / 2048 class DejaVuSansFontConstants(FontConstantsBase): - pass - - -# Maps font family names to the FontConstantBase subclass to use -_font_constant_mapping = { - 'DejaVu Sans': DejaVuSansFontConstants, - 'DejaVu Sans Mono': DejaVuSansFontConstants, - 'DejaVu Serif': DejaVuSerifFontConstants, - 'cmb10': ComputerModernFontConstants, - 'cmex10': ComputerModernFontConstants, - 'cmmi10': ComputerModernFontConstants, - 'cmr10': ComputerModernFontConstants, - 'cmss10': ComputerModernFontConstants, - 'cmsy10': ComputerModernFontConstants, - 'cmtt10': ComputerModernFontConstants, - 'STIXGeneral': STIXFontConstants, - 'STIXNonUnicode': STIXFontConstants, - 'STIXSizeFiveSym': STIXFontConstants, - 'STIXSizeFourSym': STIXFontConstants, - 'STIXSizeThreeSym': STIXFontConstants, - 'STIXSizeTwoSym': STIXFontConstants, - 'STIXSizeOneSym': STIXFontConstants, - # Map the fonts we used to ship, just for good measure - 'Bitstream Vera Sans': DejaVuSansFontConstants, - 'Bitstream Vera': DejaVuSansFontConstants, - } - - -def _get_font_constant_set(state: ParserState) -> type[FontConstantsBase]: - constants = _font_constant_mapping.get( - state.fontset._get_font(state.font).family_name, FontConstantsBase) - # STIX sans isn't really its own fonts, just different code points - # in the STIX fonts, so we have to detect this one separately. - if (constants is STIXFontConstants and - isinstance(state.fontset, StixSansFonts)): - return STIXSansFontConstants - return constants + _x_height = 1120 + x_height = _x_height / 2048 + # These values are extracted from the TeX table of DejaVuSans.ttf using FontForge, + # and then divided by design xheight, since we multiply these values by the scaled + # xheight later. + supdrop = 790.527 / _x_height + subdrop = 102.4 / _x_height + sup1 = 845.824 / _x_height + sub1 = 307.199 / _x_height + sub2 = 632.832 / _x_height + num1 = 1529.86 / _x_height + num2 = 868.352 / _x_height + num3 = 970.752 / _x_height + denom1 = 1548.29 / _x_height + denom2 = 768 / _x_height + # These come from the same TeX table, scaled by Em size so they are in multiples of + # design size. + axis_height = 512 / 2048 class Node: @@ -1022,6 +1134,15 @@ def shrink(self) -> None: def render(self, output: Output, x: float, y: float) -> None: """Render this node.""" + def is_char_node(self) -> bool: + # TeX defines a `char_node` as one which represents a single character, + # but also states that a `char_node` will never appear in a `Vlist` + # (node134). Further, nuclei made of one `Char` and nuclei made of + # multiple `Char`s have their superscripts and subscripts shifted by + # the same amount. In order to make Mathtext behave similarly, just + # check whether this node is a `Vlist` or has any `Vlist` descendants. + return True + class Box(Node): """A node with a physical location.""" @@ -1210,6 +1331,11 @@ def __init__(self, elements: T.Sequence[Node], w: float = 0.0, if do_kern: self.kern() self.hpack(w=w, m=m) + self.is_phantom = False + + def is_char_node(self) -> bool: + # See description in Node.is_char_node. + return all(map(lambda node: node.is_char_node(), self.children)) def kern(self) -> None: """ @@ -1302,6 +1428,10 @@ def __init__(self, elements: T.Sequence[Node], h: float = 0.0, super().__init__(elements) self.vpack(h=h, m=m) + def is_char_node(self) -> bool: + # See description in Node.is_char_node. + return False + def vpack(self, h: float = 0.0, m: T.Literal['additional', 'exactly'] = 'additional', l: float = np.inf) -> None: @@ -1393,7 +1523,7 @@ def __init__(self, width: float, height: float, depth: float, state: ParserState def render(self, output: Output, # type: ignore[override] x: float, y: float, w: float, h: float) -> None: - self.fontset.render_rect_filled(output, x, y, x + w, y + h) + self.fontset.render_rect_filled(output, x, y - h, w, h) class Hrule(Rule): @@ -1519,7 +1649,7 @@ class AutoHeightChar(Hlist): """ def __init__(self, c: str, height: float, depth: float, state: ParserState, - always: bool = False, factor: float | None = None): + factor: float | None = None): alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) x_height = state.fontset.get_xheight(state.font, state.fontsize, state.dpi) @@ -1556,7 +1686,7 @@ class AutoWidthChar(Hlist): always just return a scaled version of the glyph. """ - def __init__(self, c: str, width: float, state: ParserState, always: bool = False, + def __init__(self, c: str, width: float, state: ParserState, char_class: type[Char] = Char): alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) @@ -1592,6 +1722,11 @@ def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: off_v = oy + box.height output = Output(box) + phantom: list[bool] = [] + def render(node, *args): + if not any(phantom): + node.render(*args) + def clamp(value: float) -> float: return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value @@ -1605,9 +1740,11 @@ def hlist_out(box: Hlist) -> None: base_line = cur_v left_edge = cur_h + phantom.append(box.is_phantom) + for p in box.children: if isinstance(p, Char): - p.render(output, cur_h + off_h, cur_v + off_v) + render(p, output, cur_h + off_h, cur_v + off_v) cur_h += p.width elif isinstance(p, Kern): cur_h += p.width @@ -1638,9 +1775,9 @@ def hlist_out(box: Hlist) -> None: rule_depth = box.depth if rule_height > 0 and rule_width > 0: cur_v = base_line + rule_depth - p.render(output, - cur_h + off_h, cur_v + off_v, - rule_width, rule_height) + render(p, output, + cur_h + off_h, cur_v + off_v, + rule_width, rule_height) cur_v = base_line cur_h += rule_width elif isinstance(p, Glue): @@ -1658,6 +1795,8 @@ def hlist_out(box: Hlist) -> None: rule_width += cur_g cur_h += rule_width + phantom.pop() + def vlist_out(box: Vlist) -> None: nonlocal cur_v, cur_h @@ -1697,9 +1836,9 @@ def vlist_out(box: Vlist) -> None: rule_height += rule_depth if rule_height > 0 and rule_depth > 0: cur_v += rule_height - p.render(output, - cur_h + off_h, cur_v + off_v, - rule_width, rule_height) + render(p, output, + cur_h + off_h, cur_v + off_v, + rule_width, rule_height) elif isinstance(p, Glue): glue_spec = p.glue_spec rule_height = glue_spec.width - cur_g @@ -1762,7 +1901,7 @@ def font(self) -> str: @font.setter def font(self, name: str) -> None: - if name in ('rm', 'it', 'bf', 'bfit'): + if name in ('normal', 'rm', 'it', 'bf', 'bfit'): self.font_class = name self._font = name @@ -1934,7 +2073,7 @@ class _MathStyle(enum.Enum): _dropsub_symbols = set(r'\int \oint \iint \oiint \iiint \oiiint \iiiint'.split()) _fontnames = set("rm cal it tt sf bf bfit " - "default bb frak scr regular".split()) + "default bb frak scr regular normal".split()) _function_names = set(""" arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim @@ -2035,6 +2174,10 @@ def csnames(group: str, names: Iterable[str]) -> Regex: p.customspace = cmd(r"\hspace", "{" + p.float_literal("space") + "}") + p.phantom = cmd(r"\phantom", p.optional_group("value")) + p.llap = cmd(r"\llap", p.optional_group("value")) + p.rlap = cmd(r"\rlap", p.optional_group("value")) + p.accent = ( csnames("accent", [*self._accent_map, *self._wide_accents]) - p.named_placeable("sym")) @@ -2101,7 +2244,8 @@ def csnames(group: str, names: Iterable[str]) -> Regex: r"\boldsymbol", "{" + ZeroOrMore(p.simple)("value") + "}") p.placeable <<= ( - p.accent # Must be before symbol as all accents are symbols + p.phantom | p.llap | p.rlap + | p.accent # Must be before symbol as all accents are symbols | p.symbol # Must be second to catch all named symbols and single # chars not in a group | p.function @@ -2118,6 +2262,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex: | p.text | p.boldsymbol | p.substack + | p.auto_delim ) mdelim = r"\middle" - (p.delim("mdelim") | Error("Expected a delimiter")) @@ -2140,7 +2285,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex: self._math_expression = p.math # To add space to nucleus operators after sub/superscripts - self._in_subscript_or_superscript = False + self._needs_space_after_subsuper = False def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist: """ @@ -2157,7 +2302,7 @@ def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hli except ParseBaseException as err: raise ValueError("\n" + err.explain(0)) from None self._state_stack = [] - self._in_subscript_or_superscript = False + self._needs_space_after_subsuper = False # prevent operator spacing from leaking into a new expression self._em_width_cache = {} ParserElement.reset_cache() @@ -2190,7 +2335,7 @@ def non_math(self, toks: ParseResults) -> T.Any: s = toks[0].replace(r'\$', '$') symbols = [Char(c, self.get_state()) for c in s] hlist = Hlist(symbols) - # We're going into math now, so set font to 'it' + # We're going into math now, so set font to 'normal' self.push_state() self.get_state().font = mpl.rcParams['mathtext.default'] return [hlist] @@ -2209,16 +2354,14 @@ def _make_space(self, percentage: float) -> Kern: # In TeX, an em (the unit usually used to measure horizontal lengths) # is not the width of the character 'm'; it is the same in different # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in - # the italic style so that horizontal spaces don't depend on the + # the normal style so that horizontal spaces don't depend on the # current font style. + # TODO: this should be read from the font file state = self.get_state() key = (state.font, state.fontsize, state.dpi) width = self._em_width_cache.get(key) if width is None: - metrics = state.fontset.get_metrics( - 'it', mpl.rcParams['mathtext.default'], 'm', - state.fontsize, state.dpi) - width = metrics.advance + width = state.fontset.get_quad('normal', state.fontsize, state.dpi) self._em_width_cache[key] = width return Kern(width * percentage) @@ -2267,7 +2410,7 @@ def symbol(self, s: str, loc: int, prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') # Binary operators at start of string should not be spaced # Also, operators in sub- or superscripts should not be spaced - if (self._in_subscript_or_superscript or ( + if (self._needs_space_after_subsuper or ( c in self._binary_operators and ( len(s[:loc].split()) == 0 or prev_char in { '{', *self._left_delims, *self._relation_symbols}))): @@ -2296,6 +2439,16 @@ def symbol(self, s: str, loc: int, def unknown_symbol(self, s: str, loc: int, toks: ParseResults) -> T.Any: raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}") + def phantom(self, toks: ParseResults) -> T.Any: + toks["value"].is_phantom = True + return toks["value"] + + def llap(self, toks: ParseResults) -> T.Any: + return [Hlist([Kern(-toks["value"].width), toks["value"]])] + + def rlap(self, toks: ParseResults) -> T.Any: + return [Hlist([toks["value"], Kern(-toks["value"].width)])] + _accent_map = { r'hat': r'\circumflexaccent', r'breve': r'\combiningbreve', @@ -2330,12 +2483,13 @@ def accent(self, toks: ParseResults) -> T.Any: if accent in self._wide_accents: accent_box = AutoWidthChar( '\\' + accent, sym.width, state, char_class=Accent) + centered = HCentered([accent_box]) else: accent_box = Accent(self._accent_map[accent], state) - if accent == 'mathring': - accent_box.shrink() - accent_box.shrink() - centered = HCentered([Hbox(sym.width / 4.0), accent_box]) + if accent == 'mathring': + accent_box.shrink() + accent_box.shrink() + centered = HCentered([Hbox(sym.width / 4.0), accent_box]) centered.hpack(sym.width, 'exactly') return Vlist([ centered, @@ -2369,18 +2523,13 @@ def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any: next_char_loc += len('operatorname{}') next_char = next((c for c in s[next_char_loc:] if c != ' '), '') delimiters = self._delims | {'^', '_'} - if (next_char not in delimiters and - name not in self._overunder_functions): + if next_char not in delimiters: # Add thin space except when followed by parenthesis, bracket, etc. hlist_list += [self._make_space(self._space_widths[r'\,'])] self.pop_state() - # if followed by a super/subscript, set flag to true - # This flag tells subsuper to add space after this operator - if next_char in {'^', '_'}: - self._in_subscript_or_superscript = True - else: - self._in_subscript_or_superscript = False - + # If followed by a sub/superscript, set flag to true to tell subsuper + # to add space after this operator. + self._needs_space_after_subsuper = next_char in {'^', '_'} return Hlist(hlist_list) def start_group(self, toks: ParseResults) -> T.Any: @@ -2450,8 +2599,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: state = self.get_state() rule_thickness = state.fontset.get_underline_thickness( state.font, state.fontsize, state.dpi) - x_height = state.fontset.get_xheight( - state.font, state.fontsize, state.dpi) + x_height = state.fontset.get_xheight(state.font, state.fontsize, state.dpi) if napostrophes: if super is None: @@ -2490,7 +2638,10 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: shift = hlist.height + vgap + nucleus.depth vlt = Vlist(vlist) vlt.shift_amount = shift - result = Hlist([vlt]) + optional_spacing = ([self._make_space(self._space_widths[r'\,'])] + if self._needs_space_after_subsuper else []) + self._needs_space_after_subsuper = False + result = Hlist([vlt, *optional_spacing]) return [result] # We remove kerning on the last character for consistency (otherwise @@ -2519,7 +2670,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: nucleus = Hlist([nucleus]) # Handle regular sub/superscripts - consts = _get_font_constant_set(state) + consts = state.fontset.get_font_constants() lc_height = last_char.height lc_baseline = 0.0 if self.is_dropsub(last_char): @@ -2537,9 +2688,19 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: else: subkern = 0 + # Set the minimum shifts for the superscript and subscript (node756). + if nucleus.is_char_node(): + shift_up = 0.0 + shift_down = 0.0 + else: + shrunk_x_height = state.fontset.get_xheight( + state.font, state.fontsize * SHRINK_FACTOR, state.dpi) + shift_up = nucleus.height - consts.supdrop * shrunk_x_height + shift_down = nucleus.depth + consts.subdrop * shrunk_x_height + x: List if super is None: - # node757 + # Align subscript without superscript (node757). # Note: One of super or sub must be a Node if we're in this function, but # mypy can't know this, since it can't interpret pyparsing expressions, # hence the cast. @@ -2548,29 +2709,37 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: if self.is_dropsub(last_char): shift_down = lc_baseline + consts.subdrop * x_height else: - shift_down = consts.sub1 * x_height + shift_down = max(shift_down, consts.sub1 * x_height, + x.height - x_height * 4 / 5) x.shift_amount = shift_down else: + # Align superscript (node758). x = Hlist([Kern(superkern), super]) x.shrink() if self.is_dropsub(last_char): shift_up = lc_height - consts.subdrop * x_height else: - shift_up = consts.sup1 * x_height + shift_up = max(shift_up, consts.sup1 * x_height, x.depth + x_height / 4) if sub is None: x.shift_amount = -shift_up - else: # Both sub and superscript + else: + # Align subscript with superscript (node759). y = Hlist([Kern(subkern), sub]) y.shrink() if self.is_dropsub(last_char): shift_down = lc_baseline + consts.subdrop * x_height else: - shift_down = consts.sub2 * x_height - # If sub and superscript collide, move super up - clr = (2 * rule_thickness - + shift_down = max(shift_down, consts.sub2 * x_height) + # If the subscript and superscript are too close to each other, + # move the subscript down. + clr = (4 * rule_thickness - ((shift_up - x.depth) - (y.height - shift_down))) if clr > 0.: - shift_up += clr + shift_down += clr + clr = x_height * 4 / 5 - shift_up + x.depth + if clr > 0: + shift_up += clr + shift_down -= clr x = Vlist([ x, Kern((shift_up - x.depth) - (y.height - shift_down)), @@ -2582,12 +2751,10 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Do we need to add a space after the nucleus? # To find out, check the flag set by operatorname - spaced_nucleus: list[Node] = [nucleus, x] - if self._in_subscript_or_superscript: - spaced_nucleus += [self._make_space(self._space_widths[r'\,'])] - self._in_subscript_or_superscript = False - - result = Hlist(spaced_nucleus) + optional_spacing = ([self._make_space(self._space_widths[r'\,'])] + if self._needs_space_after_subsuper else []) + self._needs_space_after_subsuper = False + result = Hlist([nucleus, x, *optional_spacing]) return [result] def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle, @@ -2595,7 +2762,13 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty state = self.get_state() thickness = state.get_current_underline_thickness() + axis_height = state.fontset.get_axis_height( + state.font, state.fontsize, state.dpi) + consts = state.fontset.get_font_constants() + x_height = state.fontset.get_xheight(state.font, state.fontsize, state.dpi) + for _ in range(style.value): + x_height *= SHRINK_FACTOR num.shrink() den.shrink() cnum = HCentered([num]) @@ -2603,24 +2776,53 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty width = max(num.width, den.width) cnum.hpack(width, 'exactly') cden.hpack(width, 'exactly') - vlist = Vlist([ - cnum, # numerator - Vbox(0, 2 * thickness), # space - Hrule(state, rule), # rule - Vbox(0, 2 * thickness), # space - cden, # denominator - ]) - # Shift so the fraction line sits in the middle of the - # equals sign - metrics = state.fontset.get_metrics( - state.font, mpl.rcParams['mathtext.default'], - '=', state.fontsize, state.dpi) - shift = (cden.height - - ((metrics.ymax + metrics.ymin) / 2 - 3 * thickness)) - vlist.shift_amount = shift - - result: list[Box | Char | str] = [Hlist([vlist, Hbox(2 * thickness)])] + # Align the fraction with a fraction line (node743, node744 and node746). + if rule: + if style is self._MathStyle.DISPLAYSTYLE: + num_shift_up = consts.num1 * x_height + den_shift_down = consts.denom1 * x_height + clr = 3 * rule # The minimum clearance. + else: + num_shift_up = consts.num2 * x_height + den_shift_down = consts.denom2 * x_height + clr = rule # The minimum clearance. + delta = rule / 2 + num_clr = max((num_shift_up - cnum.depth) - (axis_height + delta), clr) + den_clr = max((axis_height - delta) - (cden.height - den_shift_down), clr) + vlist = Vlist([cnum, # numerator + Vbox(0, num_clr), # space + Hrule(state, rule), # rule + Vbox(0, den_clr), # space + cden # denominator + ]) + vlist.shift_amount = cden.height + den_clr + delta - axis_height + + # Align the fraction without a fraction line (node743, node744 and node745). + else: + if style is self._MathStyle.DISPLAYSTYLE: + num_shift_up = consts.num1 * x_height + den_shift_down = consts.denom1 * x_height + min_clr = 7 * thickness # The minimum clearance. + else: + num_shift_up = consts.num3 * x_height + den_shift_down = consts.denom2 * x_height + min_clr = 3 * thickness # The minimum clearance. + def_clr = (num_shift_up - cnum.depth) - (cden.height - den_shift_down) + clr = max(def_clr, min_clr) + vlist = Vlist([cnum, # numerator + Vbox(0, clr), # space + cden # denominator + ]) + vlist.shift_amount = den_shift_down + if def_clr < min_clr: + vlist.shift_amount += (min_clr - def_clr) / 2 + + result: list[Box | Char | str] = [Hlist([ + Hbox(thickness), + vlist, + Hbox(thickness) + ])] if ldelim or rdelim: return self._auto_sized_delimiter(ldelim or ".", result, rdelim or ".") return result @@ -2693,7 +2895,7 @@ def sqrt(self, toks: ParseResults) -> T.Any: # the height so it doesn't seem cramped height = body.height - body.shift_amount + 5 * thickness depth = body.depth + body.shift_amount - check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True) + check = AutoHeightChar(r'\__sqrt__', height, depth, state) height = check.height - check.shift_amount depth = check.depth + check.shift_amount diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 5819ee743044..6d0c20a1b2a2 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -3,9 +3,12 @@ """ from __future__ import annotations -from typing import overload +from typing import TypeAlias, overload -latex_to_bakoma = { +from .ft2font import CharacterCodeType + + +latex_to_bakoma: dict[str, tuple[str, CharacterCodeType]] = { '\\__sqrt__' : ('cmex10', 0x70), '\\bigcap' : ('cmex10', 0x5c), '\\bigcup' : ('cmex10', 0x5b), @@ -33,6 +36,75 @@ '{' : ('cmex10', 0xa9), '}' : ('cmex10', 0xaa), + '\\__angbracketleft__' : ('cmsy10', 0x68), + '\\__angbracketright__' : ('cmsy10', 0x69), + '\\__angbracketleftbig__' : ('cmex10', 0xad), + '\\__angbracketleftBig__' : ('cmex10', 0x44), + '\\__angbracketleftbigg__' : ('cmex10', 0xbf), + '\\__angbracketleftBigg__' : ('cmex10', 0x2a), + '\\__angbracketrightbig__' : ('cmex10', 0xae), + '\\__angbracketrightBig__' : ('cmex10', 0x45), + '\\__angbracketrightbigg__' : ('cmex10', 0xc0), + '\\__angbracketrightBigg__' : ('cmex10', 0x2b), + '\\__backslashbig__' : ('cmex10', 0xb2), + '\\__backslashBig__' : ('cmex10', 0x2f), + '\\__backslashbigg__' : ('cmex10', 0xc2), + '\\__backslashBigg__' : ('cmex10', 0x2d), + '\\__braceleftbig__' : ('cmex10', 0xa9), + '\\__braceleftBig__' : ('cmex10', 0x6e), + '\\__braceleftbigg__' : ('cmex10', 0xbd), + '\\__braceleftBigg__' : ('cmex10', 0x28), + '\\__bracerightbig__' : ('cmex10', 0xaa), + '\\__bracerightBig__' : ('cmex10', 0x6f), + '\\__bracerightbigg__' : ('cmex10', 0xbe), + '\\__bracerightBigg__' : ('cmex10', 0x29), + '\\__bracketleftbig__' : ('cmex10', 0xa3), + '\\__bracketleftBig__' : ('cmex10', 0x68), + '\\__bracketleftbigg__' : ('cmex10', 0x2219), + '\\__bracketleftBigg__' : ('cmex10', 0x22), + '\\__bracketrightbig__' : ('cmex10', 0xa4), + '\\__bracketrightBig__' : ('cmex10', 0x69), + '\\__bracketrightbigg__' : ('cmex10', 0xb8), + '\\__bracketrightBigg__' : ('cmex10', 0x23), + '\\__ceilingleftbig__' : ('cmex10', 0xa7), + '\\__ceilingleftBig__' : ('cmex10', 0x6c), + '\\__ceilingleftbigg__' : ('cmex10', 0xbb), + '\\__ceilingleftBigg__' : ('cmex10', 0x26), + '\\__ceilingrightbig__' : ('cmex10', 0xa8), + '\\__ceilingrightBig__' : ('cmex10', 0x6d), + '\\__ceilingrightbigg__' : ('cmex10', 0xbc), + '\\__ceilingrightBigg__' : ('cmex10', 0x27), + '\\__floorleftbig__' : ('cmex10', 0xa5), + '\\__floorleftBig__' : ('cmex10', 0x6a), + '\\__floorleftbigg__' : ('cmex10', 0xb9), + '\\__floorleftBigg__' : ('cmex10', 0x24), + '\\__floorrightbig__' : ('cmex10', 0xa6), + '\\__floorrightBig__' : ('cmex10', 0x6b), + '\\__floorrightbigg__' : ('cmex10', 0xba), + '\\__floorrightBigg__' : ('cmex10', 0x25), + '\\__hatwide__' : ('cmex10', 0x62), + '\\__hatwider__' : ('cmex10', 0x63), + '\\__hatwidest__' : ('cmex10', 0x64), + '\\__parenleftbig__' : ('cmex10', 0xa1), + '\\__parenleftBig__' : ('cmex10', 0xb3), + '\\__parenleftbigg__' : ('cmex10', 0xb5), + '\\__parenleftBigg__' : ('cmex10', 0xc3), + '\\__parenrightbig__' : ('cmex10', 0xa2), + '\\__parenrightBig__' : ('cmex10', 0xb4), + '\\__parenrightbigg__' : ('cmex10', 0xb6), + '\\__parenrightBigg__' : ('cmex10', 0x21), + '\\__radicalbig__' : ('cmex10', 0x70), + '\\__radicalBig__' : ('cmex10', 0x71), + '\\__radicalbigg__' : ('cmex10', 0x72), + '\\__radicalBigg__' : ('cmex10', 0x73), + '\\__slashbig__' : ('cmex10', 0xb1), + '\\__slashBig__' : ('cmex10', 0x2e), + '\\__slashbigg__' : ('cmex10', 0xc1), + '\\__slashBigg__' : ('cmex10', 0x2c), + '\\__tildewide__' : ('cmex10', 0x65), + '\\__tildewider__' : ('cmex10', 0x66), + '\\__tildewidest__' : ('cmex10', 0x67), + ',' : ('cmmi10', 0x3b), '.' : ('cmmi10', 0x3a), '/' : ('cmmi10', 0x3d), @@ -89,16 +161,6 @@ '(' : ('cmr10', 0x28), ')' : ('cmr10', 0x29), '+' : ('cmr10', 0x2b), - '0' : ('cmr10', 0x30), - '1' : ('cmr10', 0x31), - '2' : ('cmr10', 0x32), - '3' : ('cmr10', 0x33), - '4' : ('cmr10', 0x34), - '5' : ('cmr10', 0x35), - '6' : ('cmr10', 0x36), - '7' : ('cmr10', 0x37), - '8' : ('cmr10', 0x38), - '9' : ('cmr10', 0x39), ':' : ('cmr10', 0x3a), ';' : ('cmr10', 0x3b), '=' : ('cmr10', 0x3d), @@ -241,7 +303,7 @@ # Automatically generated. -type12uni = { +type12uni: dict[str, CharacterCodeType] = { 'aring' : 229, 'quotedblright' : 8221, 'V' : 86, @@ -475,7 +537,7 @@ # for key in sd: # print("{0:24} : {1: _EntryTypeOut: ... @@ -1735,7 +1803,7 @@ def _normalize_stix_fontcodes(d): del _stix_virtual_fonts # Fix some incorrect glyphs. -stix_glyph_fixes = { +stix_glyph_fixes: dict[CharacterCodeType, CharacterCodeType] = { # Cap and Cup glyphs are swapped. 0x22d2: 0x22d3, 0x22d3: 0x22d2, diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index b9603b114bc2..0ebbf3ac139d 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -4,46 +4,23 @@ from __future__ import annotations -import dataclasses +from collections.abc import Iterator from . import _api -from .ft2font import FT2Font, Kerning, LoadFlags +from .ft2font import FT2Font, CharacterCodeType, LayoutItem, LoadFlags -@dataclasses.dataclass(frozen=True) -class LayoutItem: - ft_object: FT2Font - char: str - glyph_idx: int - x: float - prev_kern: float - - -def warn_on_missing_glyph(codepoint, fontnames): +def warn_on_missing_glyph(codepoint: CharacterCodeType, fontnames: str): _api.warn_external( f"Glyph {codepoint} " f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) " f"missing from font(s) {fontnames}.") - block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else - "Arabic" if 0x0600 <= codepoint <= 0x06ff else - "Devanagari" if 0x0900 <= codepoint <= 0x097f else - "Bengali" if 0x0980 <= codepoint <= 0x09ff else - "Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else - "Gujarati" if 0x0a80 <= codepoint <= 0x0aff else - "Oriya" if 0x0b00 <= codepoint <= 0x0b7f else - "Tamil" if 0x0b80 <= codepoint <= 0x0bff else - "Telugu" if 0x0c00 <= codepoint <= 0x0c7f else - "Kannada" if 0x0c80 <= codepoint <= 0x0cff else - "Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else - "Sinhala" if 0x0d80 <= codepoint <= 0x0dff else - None) - if block: - _api.warn_external( - f"Matplotlib currently does not support {block} natively.") - -def layout(string, font, *, kern_mode=Kerning.DEFAULT): +def layout(string: str, font: FT2Font, *, + features: tuple[str] | None = None, + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> Iterator[LayoutItem]: """ Render *string* with *font*. @@ -56,27 +33,18 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): The string to be rendered. font : FT2Font The font. - kern_mode : Kerning - A FreeType kerning mode. + features : tuple of str, optional + The font features to apply to the text. + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Yields ------ LayoutItem """ - x = 0 - prev_glyph_idx = None - char_to_font = font._get_fontmap(string) - base_font = font - for char in string: - # This has done the fallback logic - font = char_to_font.get(char, base_font) - glyph_idx = font.get_char_index(ord(char)) - kern = ( - base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64 - if prev_glyph_idx is not None else 0. - ) - x += kern - glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING) - yield LayoutItem(font, char, glyph_idx, x, kern) - x += glyph.linearHoriAdvance / 65536 - prev_glyph_idx = glyph_idx + for raqm_item in font._layout(string, LoadFlags.NO_HINTING, + features=features, language=language): + raqm_item.ft_object.load_glyph(raqm_item.glyph_index, + flags=LoadFlags.NO_HINTING) + yield raqm_item diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a2a878d54156..a06779b8efee 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -2,9 +2,12 @@ Common functionality between the PDF and PS backends. """ +from __future__ import annotations + from io import BytesIO import functools import logging +import typing from fontTools import subset @@ -14,24 +17,35 @@ from ..backend_bases import RendererBase +if typing.TYPE_CHECKING: + from .ft2font import CharacterCodeType, FT2Font, GlyphIndexType + from fontTools.ttLib import TTFont + + +_FONT_MAX_GLYPH = { + 3: 256, + 42: 65536, +} + + @functools.lru_cache(50) def _cached_get_afm_from_fname(fname): with open(fname, "rb") as fh: return AFM(fh) -def get_glyphs_subset(fontfile, characters): +def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: """ - Subset a TTF font + Subset a TTF font. - Reads the named fontfile and restricts the font to the characters. + Reads the named fontfile and restricts the font to the glyphs. Parameters ---------- - fontfile : str + fontfile : FontPath Path to the font file - characters : str - Continuous set of characters to include in subset + glyphs : set[GlyphIndexType] + Set of glyph indices to include in subset. Returns ------- @@ -39,8 +53,8 @@ def get_glyphs_subset(fontfile, characters): An open font object representing the subset, which needs to be closed by the caller. """ - - options = subset.Options(glyph_names=True, recommended_glyphs=True) + options = subset.Options(glyph_names=True, recommended_glyphs=True, + retain_gids=True) # Prevent subsetting extra tables. options.drop_tables += [ @@ -66,12 +80,11 @@ def get_glyphs_subset(fontfile, characters): 'xref', # The cross-reference table (some Apple font tooling information). ] # if fontfile is a ttc, specify font number - if fontfile.endswith(".ttc"): - options.font_number = 0 + options.font_number = fontfile.face_index font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) - subsetter.populate(text=characters) + subsetter.populate(gids=glyphs) subsetter.subset(font) return font @@ -95,26 +108,217 @@ def font_as_file(font): return fh -class CharacterTracker: +class GlyphMap: + """ + A two-way glyph mapping. + + The forward glyph map is from (character string, glyph index)-pairs to + (subset index, subset character code)-pairs. + + The inverse glyph map is from to (subset index, subset character code)-pairs to + (character string, glyph index)-pairs. """ - Helper for font subsetting by the pdf and ps backends. - Maintains a mapping of font paths to the set of character codepoints that - are being used from that font. + def __init__(self) -> None: + self._forward: dict[tuple[CharacterCodeType, GlyphIndexType], + tuple[int, CharacterCodeType]] = {} + self._inverse: dict[tuple[int, CharacterCodeType], + tuple[CharacterCodeType, GlyphIndexType]] = {} + + def get(self, charcodes: str, + glyph_index: GlyphIndexType) -> tuple[int, CharacterCodeType] | None: + """ + Get the forward mapping from a (character string, glyph index)-pair. + + This may return *None* if the pair is not currently mapped. + """ + return self._forward.get((charcodes, glyph_index)) + + def iget(self, subset: int, + subset_charcode: CharacterCodeType) -> tuple[str, GlyphIndexType]: + """Get the inverse mapping from a (subset, subset charcode)-pair.""" + return self._inverse[(subset, subset_charcode)] + + def add(self, charcode: str, glyph_index: GlyphIndexType, subset: int, + subset_charcode: CharacterCodeType) -> None: + """ + Add a mapping to this instance. + + Parameters + ---------- + charcode : CharacterCodeType + The character code to record. + glyph : GlyphIndexType + The corresponding glyph index to record. + subset : int + The subset in which the subset character code resides. + subset_charcode : CharacterCodeType + The subset character code within the above subset. + """ + self._forward[(charcode, glyph_index)] = (subset, subset_charcode) + self._inverse[(subset, subset_charcode)] = (charcode, glyph_index) + + +class CharacterTracker: """ + Helper for font subsetting by the PDF and PS backends. - def __init__(self): - self.used = {} + Maintains a mapping of font paths to the set of characters and glyphs that are being + used from that font. - def track(self, font, s): - """Record that string *s* is being typeset using font *font*.""" - char_to_font = font._get_fontmap(s) - for _c, _f in char_to_font.items(): - self.used.setdefault(_f.fname, set()).add(ord(_c)) + Attributes + ---------- + subset_size : int + The size at which characters are grouped into subsets. + used : dict + A dictionary of font files to character maps. + + The key is a font filename. + + The value is a list of dictionaries, each mapping at most *subset_size* + character codes to glyph indices. Note this mapping is the inverse of FreeType, + which maps glyph indices to character codes. + + If *subset_size* is not set, then there will only be one subset per font + filename. + glyph_maps : dict + A dictionary of font files to glyph maps. You probably will want to use the + `.subset_to_unicode` method instead of this attribute. + """ - def track_glyph(self, font, glyph): - """Record that codepoint *glyph* is being typeset using font *font*.""" - self.used.setdefault(font.fname, set()).add(glyph) + def __init__(self, subset_size: int = 0): + """ + Parameters + ---------- + subset_size : int, optional + The maximum size that is supported for an embedded font. If provided, then + characters will be grouped into these sized subsets. + """ + self.used: dict[str, list[dict[CharacterCodeType, GlyphIndexType]]] = {} + self.glyph_maps: dict[str, GlyphMap] = {} + self.subset_size = subset_size + + def track(self, font: FT2Font, s: str, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> list[tuple[int, CharacterCodeType]]: + """ + Record that string *s* is being typeset using font *font*. + + Parameters + ---------- + font : FT2Font + A font that is being used for the provided string. + s : str + The string that should be marked as tracked by the provided font. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + Returns + ------- + list[tuple[int, CharacterCodeType]] + A list of subset and character code pairs corresponding to the input string. + If a *subset_size* is specified on this instance, then the character code + will correspond with the given subset (and not necessarily the string as a + whole). If *subset_size* is not specified, then the subset will always be 0 + and the character codes will be returned from the string unchanged. + """ + return [ + self.track_glyph(raqm_item.ft_object, raqm_item.char, raqm_item.glyph_index) + for raqm_item in font._layout(s, ft2font.LoadFlags.NO_HINTING, + features=features, language=language) + ] + + def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, + glyph: GlyphIndexType) -> tuple[int, CharacterCodeType]: + """ + Record character code *charcode* at glyph index *glyph* as using font *font*. + + Parameters + ---------- + font : FT2Font + A font that is being used for the provided string. + chars : str or CharacterCodeType + The character(s) to record. This may be a single character code, or multiple + characters in a string, if the glyph maps to several characters. It will be + normalized to a string internally. + glyph : GlyphIndexType + The corresponding glyph index to record. + + Returns + ------- + subset : int + The subset in which the returned character code resides. If *subset_size* + was not specified on this instance, then this is always 0. + subset_charcode : CharacterCodeType + The character code within the above subset. If *subset_size* was not + specified on this instance, then this is just *charcode* unmodified. + """ + if isinstance(chars, str): + charcode = ord(chars[0]) + else: + charcode = chars + chars = chr(chars) + + font_path = font_manager.FontPath(font.fname, font.face_index) + glyph_map = self.glyph_maps.setdefault(font_path, GlyphMap()) + if result := glyph_map.get(chars, glyph): + return result + + subset_maps = self.used.setdefault(font_path, [{}]) + use_next_charmap = ( + # Multi-character glyphs always go in the non-0 subset. + len(chars) > 1 or + # Default to preserving the character code as it was. + self.subset_size != 0 + and ( + # But start filling a new subset if outside the first block; this + # preserves ASCII (for Type 3) or the Basic Multilingual Plane (for + # Type 42). + charcode >= self.subset_size + # Or, use a new subset if the character code is already mapped for the + # first block. This means it's using an alternate glyph. + or charcode in subset_maps[0] + ) + ) + if use_next_charmap: + if len(subset_maps) == 1 or len(subset_maps[-1]) == self.subset_size: + subset_maps.append({}) + subset = len(subset_maps) - 1 + subset_charcode = len(subset_maps[-1]) + else: + subset = 0 + subset_charcode = charcode + subset_maps[subset][subset_charcode] = glyph + glyph_map.add(chars, glyph, subset, subset_charcode) + return (subset, subset_charcode) + + def subset_to_unicode(self, fontname: str, subset: int, + subset_charcode: CharacterCodeType) -> str: + """ + Map a subset index and character code to a Unicode character code. + + Parameters + ---------- + fontname : str + The name of the font, from the *used* dictionary key. + subset : int + The subset index within a font. + subset_charcode : CharacterCodeType + The character code within a subset to map back. + + Returns + ------- + str + The Unicode character(s) corresponding to the subsetted character code. + """ + return self.glyph_maps[fontname].iget(subset, subset_charcode)[0] class RendererPDFPSBase(RendererBase): @@ -144,6 +348,32 @@ def get_canvas_width_height(self): # docstring inherited return self.width * 72.0, self.height * 72.0 + def _get_font_height_metrics(self, prop): + """ + Return the ascent, descent, and line gap for font described by *prop*. + + TODO: This is a temporary method until we design a proper API for the backends. + + Parameters + ---------- + prop : `.font_manager.FontProperties` + The properties describing the font to measure. + + Returns + ------- + ascent, descent, line_gap : float or None + The ascent, descent and line gap of the determined font, or None to fall + back to normal measurements. + """ + if not mpl.rcParams[self._use_afm_rc_name]: + return None, None, None + font = self._get_font_afm(prop) + scale = prop.get_size_in_points() / 1000 + a = font.get_ascender() * scale + d = -font.get_descender() * scale + g = (a + d) * 0.2 # Preserve previous line spacing of 1.2. + return a, d, g + def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath == "TeX": diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 33b0be18ca2d..f0006a2d7dbe 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -22,7 +22,7 @@ """ from contextlib import nullcontext -from math import radians, cos, sin +import math import numpy as np from PIL import features @@ -31,8 +31,9 @@ from matplotlib import _api, cbook from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) +from matplotlib.dviread import Dvi from matplotlib.font_manager import fontManager as _fontManager, get_font -from matplotlib.ft2font import LoadFlags +from matplotlib.ft2font import LoadFlags, RenderMode from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase @@ -71,7 +72,7 @@ def __init__(self, width, height, dpi): self._filter_renderers = [] self._update_methods() - self.mathtext_parser = MathTextParser('agg') + self.mathtext_parser = MathTextParser('path') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) @@ -171,38 +172,83 @@ def draw_path(self, gc, path, transform, rgbFace=None): raise OverflowError(msg) from None + def _draw_text_glyphs_and_boxes(self, gc, x, y, angle, glyphs, boxes): + # y is downwards. + cos = math.cos(math.radians(angle)) + sin = math.sin(math.radians(angle)) + load_flags = get_hinting_flag() + for font, size, glyph_index, slant, extend, dx, dy in glyphs: # dy is upwards. + font.set_size(size, self.dpi) + hf = font._hinting_factor + font._set_transform( + (0x10000 * np.array([[cos, -sin], [sin, cos]]) + @ [[extend, extend * slant], [0, 1]] + @ [[1 / hf, 0], [0, 1]]).round().astype(int), + [round(0x40 * (x + dx * cos - dy * sin)), + # FreeType's y is upwards. + round(0x40 * (self.height - y + dx * sin + dy * cos))] + ) + bitmap = font._render_glyph( + glyph_index, load_flags, + RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO) + buffer = bitmap.buffer + if not gc.get_antialiased(): + buffer *= 0xff + # draw_text_image's y is downwards & the bitmap bottom side. + self._renderer.draw_text_image( + buffer, + bitmap.left, int(self.height) - bitmap.top + buffer.shape[0], + 0, gc) + + rgba = gc.get_rgb() + if len(rgba) == 3 or gc.get_forced_alpha(): + rgba = rgba[:3] + (gc.get_alpha(),) + gc1 = self.new_gc() + gc1.set_linewidth(0) + gc1.set_snap(gc.get_snap()) + for dx, dy, w, h in boxes: # dy is upwards. + if gc1.get_snap() in [None, True]: + # Prevent thin bars from disappearing by growing symmetrically. + if w < 1: + dx -= (1 - w) / 2 + w = 1 + if h < 1: + dy -= (1 - h) / 2 + h = 1 + path = Path._create_closed( + [(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)]) + self._renderer.draw_path( + gc1, path, + mpl.transforms.Affine2D() + .rotate_deg(angle).translate(x, self.height - y), + rgba) + gc1.restore() + def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw mathtext using :mod:`matplotlib.mathtext`.""" - ox, oy, width, height, descent, font_image = \ - self.mathtext_parser.parse(s, self.dpi, prop, - antialiased=gc.get_antialiased()) - - xd = descent * sin(radians(angle)) - yd = descent * cos(radians(angle)) - x = round(x + ox + xd) - y = round(y - oy + yd) - self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) + parse = self.mathtext_parser.parse( + s, self.dpi, prop, antialiased=gc.get_antialiased()) + self._draw_text_glyphs_and_boxes( + gc, x, y, angle, + ((font, size, glyph_index, 0, 1, dx, dy) + for font, size, _char, glyph_index, dx, dy in parse.glyphs), + parse.rects) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._prepare_font(prop) - # We pass '0' for angle here, since it will be rotated (in raster - # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=get_hinting_flag()) - font.draw_glyphs_to_bitmap( - antialiased=gc.get_antialiased()) - d = font.get_descent() / 64.0 - # The descent needs to be adjusted for the angle. - xo, yo = font.get_bitmap_offset() - xo /= 64.0 - yo /= 64.0 - xd = d * sin(radians(angle)) - yd = d * cos(radians(angle)) - x = round(x + xo + xd) - y = round(y + yo + yd) - self._renderer.draw_text_image(font, x, y + 1, angle, gc) + items = font._layout( + s, flags=get_hinting_flag(), + features=mtext.get_fontfeatures() if mtext is not None else None, + language=mtext.get_language() if mtext is not None else None) + size = prop.get_size_in_points() + self._draw_text_glyphs_and_boxes( + gc, x, y, angle, + ((item.ft_object, size, item.glyph_index, 0, 1, item.x, item.y) + for item in items), + []) def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited @@ -212,9 +258,8 @@ def get_text_width_height_descent(self, s, prop, ismath): return super().get_text_width_height_descent(s, prop, ismath) if ismath: - ox, oy, width, height, descent, font_image = \ - self.mathtext_parser.parse(s, self.dpi, prop) - return width, height, descent + parse = self.mathtext_parser.parse(s, self.dpi, prop) + return parse.width, parse.height, parse.depth font = self._prepare_font(prop) font.set_text(s, 0.0, flags=get_hinting_flag()) @@ -228,19 +273,31 @@ def get_text_width_height_descent(self, s, prop, ismath): def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited # todo, handle props, angle, origins - size = prop.get_size_in_points() - - texmanager = self.get_texmanager() - Z = texmanager.get_grey(s, size, self.dpi) - Z = np.array(Z * 255.0, np.uint8) + size = prop.get_size_in_points() - w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") - xd = d * sin(radians(angle)) - yd = d * cos(radians(angle)) - x = round(x + xd) - y = round(y + yd) - self._renderer.draw_text_image(Z, x, y, angle, gc) + if mpl.rcParams["text.latex.engine"] == "latex+dvipng": + Z = self.get_texmanager().get_grey(s, size, self.dpi) + Z = (Z * 0xff).astype(np.uint8) + w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") + xd = d * math.sin(math.radians(angle)) + yd = d * math.cos(math.radians(angle)) + x = round(x + xd) + y = round(y + yd) + self._renderer.draw_text_image(Z, x, y, angle, gc) + return + + dvifile = self.get_texmanager().make_dvi(s, size) + with Dvi(dvifile, self.dpi) as dvi: + page, = dvi + + self._draw_text_glyphs_and_boxes( + gc, x, y, angle, + ((get_font(text.font_path), text.font_size, text.index, + text.font_effects.get('slant', 0), text.font_effects.get('extend', 1), + text.x, text.y) + for text in page.text), + ((box.x, box.y, box.width, box.height) for box in page.boxes)) def get_canvas_width_height(self): # docstring inherited diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 15cb3a9e6708..a62890d7c3b1 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -8,6 +8,7 @@ import functools import gzip +import itertools import math import numpy as np @@ -248,13 +249,12 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if angle: ctx.rotate(np.deg2rad(-angle)) - for font, fontsize, idx, ox, oy in glyphs: + for (font, fontsize), font_glyphs in itertools.groupby( + glyphs, key=lambda info: (info[0], info[1])): ctx.new_path() - ctx.move_to(ox, -oy) - ctx.select_font_face( - *_cairo_font_args_from_font_prop(ttfFontProperty(font))) + ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_text(chr(idx)) + ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) for ox, oy, w, h in rects: ctx.new_path() diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e4bb86d1f6b5..280a72d534ad 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -19,7 +19,6 @@ import sys import time import types -import warnings import zlib import numpy as np @@ -33,9 +32,9 @@ RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure -from matplotlib.font_manager import get_font, fontManager as _fontManager +from matplotlib.font_manager import FontPath, get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags +from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -369,21 +368,6 @@ def pdfRepr(obj): "objects") -def _font_supports_glyph(fonttype, glyph): - """ - Returns True if the font is able to provide codepoint *glyph* in a PDF. - - For a Type 3 font, this method returns True only for single-byte - characters. For Type 42 fonts this method return True if the character is - from the Basic Multilingual Plane. - """ - if fonttype == 3: - return glyph <= 255 - if fonttype == 42: - return glyph <= 65535 - raise NotImplementedError() - - class Reference: """ PDF reference object. @@ -485,6 +469,7 @@ class Op(Enum): textpos = b'Td' selectfont = b'Tf' textmatrix = b'Tm' + textrise = b'Ts' show = b'Tj' showkern = b'TJ' setlinewidth = b'w' @@ -611,46 +596,24 @@ def _flush(self): self.compressobj = None -def _get_pdf_charprocs(font_path, glyph_ids): +def _get_pdf_charprocs(font_path, glyph_indices): font = get_font(font_path, hinting_factor=1) conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's). procs = {} - for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) - # NOTE: We should be using round(), but instead use - # "(x+.5).astype(int)" to keep backcompat with the old ttconv code - # (this is different for negative x's). - d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) + d1 = [ + round(g.horiAdvance * conv), 0, + # Round bbox corners *outwards*, so that they indeed bound the glyph. + math.floor(g.bbox[0] * conv), math.floor(g.bbox[1] * conv), + math.ceil(g.bbox[2] * conv), math.ceil(g.bbox[3] * conv), + ] v, c = font.get_path() - v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's). - # Backcompat with old ttconv code: control points between two quads are - # omitted if they are exactly at the midpoint between the control of - # the quad before and the quad after, but ttconv used to interpolate - # *after* conversion to PS units, causing floating point errors. Here - # we reproduce ttconv's logic, detecting these "implicit" points and - # re-interpolating them. Note that occasionally (e.g. with DejaVu Sans - # glyph "0") a point detected as "implicit" is actually explicit, and - # will thus be shifted by 1. - quads, = np.nonzero(c == 3) - quads_on = quads[1::2] - quads_mid_on = np.array( - sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int) - implicit = quads_mid_on[ - (v[quads_mid_on] # As above, use astype(int), not // division - == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int)) - .all(axis=1)] - if (font.postscript_name, glyph_id) in [ - ("DejaVuSerif-Italic", 77), # j - ("DejaVuSerif-Italic", 135), # \AA - ]: - v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1). - v = (v * conv + .5).astype(int) # As above re: truncation vs rounding. - v[implicit] = (( # Fix implicit points; again, truncate. - (v[implicit - 1] + v[implicit + 1]) / 2).astype(int)) - procs[font.get_glyph_name(glyph_id)] = ( + v = (v * 64 * conv).round() # Back to TrueType's internal units (1/64's). + procs[font.get_glyph_name(glyph_index)] = ( " ".join(map(str, d1)).encode("ascii") + b" d1\n" + _path.convert_to_string( - Path(v, c), None, None, False, None, -1, + Path(v, c), None, None, False, None, 0, # no code for quad Beziers triggers auto-conversion to cubics. [b"m", b"l", b"", b"c", b"h"], True) + b"f") @@ -722,7 +685,8 @@ def __init__(self, filename, metadata=None): self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1)) self._fontNames = {} # maps filenames to internal font names self._dviFontInfo = {} # maps pdf names to dvifonts - self._character_tracker = _backend_pdf_ps.CharacterTracker() + self._character_tracker = _backend_pdf_ps.CharacterTracker( + _backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0)) self.alphaStates = {} # maps alpha values to graphics state objects self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) @@ -737,7 +701,6 @@ def __init__(self, filename, metadata=None): self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) self.markers = {} - self.multi_byte_charprocs = {} self.paths = [] @@ -764,6 +727,7 @@ def __init__(self, filename, metadata=None): self.writeObject(self.resourceObject, resources) fontNames = _api.deprecated("3.11")(property(lambda self: self._fontNames)) + multi_byte_charprocs = _api.deprecated("3.11")(property(lambda _: {})) type1Descriptors = _api.deprecated("3.11")(property(lambda _: {})) @_api.deprecated("3.11") @@ -851,7 +815,7 @@ def toStr(n, base): @staticmethod def _get_subsetted_psname(ps_name, charmap): - return PdfFile._get_subset_prefix(frozenset(charmap.keys())) + ps_name + return PdfFile._get_subset_prefix(frozenset(charmap.values())) + ps_name def finalize(self): """Write out the various deferred objects and the pdf end matter.""" @@ -867,8 +831,6 @@ def finalize(self): name: ob for image, name, ob in self._images.values()} for tup in self.markers.values(): xobjects[tup[0]] = tup[1] - for name, value in self.multi_byte_charprocs.items(): - xobjects[name] = value for name, path, trans, ob, join, cap, padding, filled, stroked \ in self.paths: xobjects[name] = ob @@ -925,15 +887,17 @@ def _write_annotations(self): for annotsObject, annotations in self._annotations: self.writeObject(annotsObject, annotations) - def fontName(self, fontprop): + def fontName(self, fontprop, subset=0): """ Select a font based on fontprop and return a name suitable for ``Op.selectfont``. If fontprop is a string, it will be interpreted as the filename of the font. """ - if isinstance(fontprop, str): + if isinstance(fontprop, FontPath): filenames = [fontprop] + elif isinstance(fontprop, str): + filenames = [FontPath(fontprop, 0)] elif mpl.rcParams['pdf.use14corefonts']: filenames = _fontManager._find_fonts_by_props( fontprop, fontext='afm', directory=RendererPdf._afm_font_dir @@ -942,13 +906,13 @@ def fontName(self, fontprop): filenames = _fontManager._find_fonts_by_props(fontprop) first_Fx = None for fname in filenames: - Fx = self._fontNames.get(fname) + Fx = self._fontNames.get((fname, subset)) if not first_Fx: first_Fx = Fx if Fx is None: Fx = next(self._internal_font_seq) - self._fontNames[fname] = Fx - _log.debug('Assigning font %s = %r', Fx, fname) + self._fontNames[(fname, subset)] = Fx + _log.debug('Assigning font %s (subset %d) = %r', Fx, subset, fname) if not first_Fx: first_Fx = Fx @@ -972,9 +936,8 @@ def writeFonts(self): for pdfname, dvifont in sorted(self._dviFontInfo.items()): _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) - for filename in sorted(self._fontNames): - Fx = self._fontNames[filename] - _log.debug('Embedding font %s.', filename) + for (filename, subset), Fx in sorted(self._fontNames.items()): + _log.debug('Embedding font %r:%d.', filename, subset) if filename.endswith('.afm'): # from pdf.use14corefonts _log.debug('Writing AFM font.') @@ -982,9 +945,8 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - chars = self._character_tracker.used.get(filename) - if chars: - fonts[Fx] = self.embedTTF(filename, chars) + charmap = self._character_tracker.used[filename][subset] + fonts[Fx] = self.embedTTF(filename, subset, charmap) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1026,8 +988,14 @@ def _embedTeXFont(self, dvifont): # Reduce the font to only the glyphs used in the document, get the encoding # for that subset, and compute various properties based on the encoding. - chars = frozenset(self._character_tracker.used[dvifont.fname]) - t1font = t1font.subset(chars, self._get_subset_prefix(chars)) + font_path = FontPath(dvifont.fname, dvifont.face_index) + charmap = self._character_tracker.used[font_path][0] + chars = { + # DVI type 1 fonts always map single glyph to single character. + ord(self._character_tracker.subset_to_unicode(font_path, 0, ccode)) + for ccode in charmap + } + t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) fontdict['BaseFont'] = Name(t1font.prop['FontName']) # createType1Descriptor writes the font data as a side effect fontdict['FontDescriptor'] = self.createType1Descriptor(t1font) @@ -1129,13 +1097,6 @@ def createType1Descriptor(self, t1font, fontfile=None): return fontdescObject - def _get_xobject_glyph_name(self, filename, glyph_name): - Fx = self.fontName(filename) - return "-".join([ - Fx.name.decode(), - os.path.splitext(os.path.basename(filename))[0], - glyph_name]) - _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin begincmap @@ -1157,9 +1118,8 @@ def _get_xobject_glyph_name(self, filename, glyph_name): end end""" - def embedTTF(self, filename, characters): + def embedTTF(self, filename, subset_index, charmap): """Embed the TTF font from the named file into the document.""" - font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1174,14 +1134,44 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, characters, descriptor): + def generate_unicode_cmap(subset_index, charmap): + # Make the ToUnicode CMap. + last_ccode = -2 + unicode_groups = [] + for ccode in sorted(charmap.keys()): + if ccode != last_ccode + 1: + unicode_groups.append([ccode, ccode]) + else: + unicode_groups[-1][1] = ccode + last_ccode = ccode + + def _to_unicode(ccode): + chars = self._character_tracker.subset_to_unicode( + filename, subset_index, ccode) + hexstr = chars.encode('utf-16be').hex() + return f'<{hexstr}>' + + width = 2 if fonttype == 3 else 4 + unicode_bfrange = [] + for start, end in unicode_groups: + real_values = ' '.join(_to_unicode(x) for x in range(start, end+1)) + unicode_bfrange.append( + f'<{start:0{width}x}> <{end:0{width}x}> [{real_values}]') + unicode_cmap = (self._identityToUnicodeCMap % + (len(unicode_groups), + '\n'.join(unicode_bfrange).encode('ascii'))) + + return unicode_cmap + + def embedTTFType3(font, subset_index, charmap, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') fontdictObject = self.reserveObject('font dictionary') charprocsObject = self.reserveObject('character procs') + toUnicodeMapObject = self.reserveObject('ToUnicode map') differencesArray = [] - firstchar, lastchar = 0, 255 + firstchar, lastchar = min(charmap), max(charmap) bbox = [cvt(x, nearest=False) for x in font.bbox] fontdict = { @@ -1198,43 +1188,25 @@ def embedTTFType3(font, characters, descriptor): 'Encoding': { 'Type': Name('Encoding'), 'Differences': differencesArray}, - 'Widths': widthsObject - } - - from encodings import cp1252 + 'Widths': widthsObject, + 'ToUnicode': toUnicodeMapObject, + } # Make the "Widths" array def get_char_width(charcode): - s = ord(cp1252.decoding_table[charcode]) - width = font.load_char( - s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance + width = font.load_glyph( + charmap.get(charcode, 0), + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) - with warnings.catch_warnings(): - # Ignore 'Required glyph missing from current font' warning - # from ft2font: here we're just building the widths table, but - # the missing glyphs may not even be used in the actual string. - warnings.filterwarnings("ignore") - widths = [get_char_width(charcode) - for charcode in range(firstchar, lastchar+1)] + widths = [get_char_width(charcode) + for charcode in range(firstchar, lastchar+1)] descriptor['MaxWidth'] = max(widths) - # Make the "Differences" array, sort the ccodes < 255 from - # the multi-byte ccodes, and build the whole set of glyph ids - # that we need from this font. - glyph_ids = [] - differences = [] - multi_byte_chars = set() - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph_ids.append(gind) - glyph_name = font.get_glyph_name(gind) - if ccode <= 255: - differences.append((ccode, glyph_name)) - else: - multi_byte_chars.add(glyph_name) - differences.sort() - + # Make the "Differences" array with the whole set of character codes that we + # need from this font. + differences = sorted([ + (ccode, font.get_glyph_name(gind)) for ccode, gind in charmap.items() + ]) last_c = -2 for c, name in differences: if c != last_c + 1: @@ -1243,44 +1215,26 @@ def get_char_width(charcode): last_c = c # Make the charprocs array. - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + rawcharprocs = _get_pdf_charprocs(filename, charmap.values()) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] - charprocDict = {} - # The 2-byte characters are used as XObjects, so they - # need extra info in their dictionary - if charname in multi_byte_chars: - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} - # Each glyph includes bounding box information, - # but xpdf and ghostscript can't handle it in a - # Form XObject (they segfault!!!), so we remove it - # from the stream here. It's not needed anyway, - # since the Form XObject includes it in its BBox - # value. - stream = stream[stream.find(b"d1") + 2:] charprocObject = self.reserveObject('charProc') - self.outputStream(charprocObject, stream, extra=charprocDict) + self.outputStream(charprocObject, stream) + charprocs[charname] = charprocObject - # Send the glyphs with ccode > 255 to the XObject dictionary, - # and the others to the font itself - if charname in multi_byte_chars: - name = self._get_xobject_glyph_name(filename, charname) - self.multi_byte_charprocs[name] = charprocObject - else: - charprocs[charname] = charprocObject + unicode_cmap = generate_unicode_cmap(subset_index, charmap) # Write everything out self.writeObject(fontdictObject, fontdict) self.writeObject(fontdescObject, descriptor) self.writeObject(widthsObject, widths) self.writeObject(charprocsObject, charprocs) + self.outputStream(toUnicodeMapObject, unicode_cmap) return fontdictObject - def embedTTFType42(font, characters, descriptor): + def embedTTFType42(font, subset_index, charmap, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1290,18 +1244,15 @@ def embedTTFType42(font, characters, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - subset_str = "".join(chr(c) for c in characters) - _log.debug("SUBSET %s characters: %s", filename, subset_str) - with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: + _log.debug("SUBSET %r:%d characters: %s", filename, subset_index, charmap) + with _backend_pdf_ps.get_glyphs_subset(filename, + charmap.values()) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s %d -> %d", filename, + "SUBSET %r:%d %d -> %d", filename, subset_index, os.stat(filename).st_size, fontdata.getbuffer().nbytes ) - # We need this ref for XObjects - full_font = font - # reload the font object from the subset # (all the necessary data could probably be obtained directly # using fontLib.ttLib) @@ -1335,19 +1286,15 @@ def embedTTFType42(font, characters, descriptor): fontfileObject, fontdata.getvalue(), extra={'Length1': fontdata.getbuffer().nbytes}) - # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap - # at the same time + # Make the 'W' (Widths) array and CidToGidMap at the same time. cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph = font.load_char(ccode, - flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + for ccode, gind in charmap.items(): + glyph = font.load_glyph(gind, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) widths.append((ccode, cvt(glyph.horiAdvance))) - if ccode < 65536: - cid_to_gid_map[ccode] = chr(gind) + cid_to_gid_map[ccode] = chr(gind) max_ccode = max(ccode, max_ccode) widths.sort() cid_to_gid_map = cid_to_gid_map[:max_ccode + 1] @@ -1355,64 +1302,21 @@ def embedTTFType42(font, characters, descriptor): last_ccode = -2 w = [] max_width = 0 - unicode_groups = [] for ccode, width in widths: if ccode != last_ccode + 1: w.append(ccode) w.append([width]) - unicode_groups.append([ccode, ccode]) else: w[-1].append(width) - unicode_groups[-1][1] = ccode max_width = max(max_width, width) last_ccode = ccode - unicode_bfrange = [] - for start, end in unicode_groups: - # Ensure the CID map contains only chars from BMP - if start > 65535: - continue - end = min(65535, end) - - unicode_bfrange.append( - b"<%04x> <%04x> [%s]" % - (start, end, - b" ".join(b"<%04x>" % x for x in range(start, end+1)))) - unicode_cmap = (self._identityToUnicodeCMap % - (len(unicode_groups), b"\n".join(unicode_bfrange))) - - # Add XObjects for unsupported chars - glyph_ids = [] - for ccode in characters: - if not _font_supports_glyph(fonttype, ccode): - gind = full_font.get_char_index(ccode) - glyph_ids.append(gind) - - bbox = [cvt(x, nearest=False) for x in full_font.bbox] - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) - for charname in sorted(rawcharprocs): - stream = rawcharprocs[charname] - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} - # Each glyph includes bounding box information, - # but xpdf and ghostscript can't handle it in a - # Form XObject (they segfault!!!), so we remove it - # from the stream here. It's not needed anyway, - # since the Form XObject includes it in its BBox - # value. - stream = stream[stream.find(b"d1") + 2:] - charprocObject = self.reserveObject('charProc') - self.outputStream(charprocObject, stream, extra=charprocDict) - - name = self._get_xobject_glyph_name(filename, charname) - self.multi_byte_charprocs[name] = charprocObject - # CIDToGIDMap stream cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be") self.outputStream(cidToGidMapObject, cid_to_gid_map) # ToUnicode CMap + unicode_cmap = generate_unicode_cmap(subset_index, charmap) self.outputStream(toUnicodeMapObject, unicode_cmap) descriptor['MaxWidth'] = max_width @@ -1427,10 +1331,7 @@ def embedTTFType42(font, characters, descriptor): # Beginning of main embedTTF function... - ps_name = self._get_subsetted_psname( - font.postscript_name, - font.get_charmap() - ) + ps_name = self._get_subsetted_psname(font.postscript_name, charmap) ps_name = ps_name.encode('ascii', 'replace') ps_name = Name(ps_name) pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} @@ -1471,9 +1372,9 @@ def embedTTFType42(font, characters, descriptor): } if fonttype == 3: - return embedTTFType3(font, characters, descriptor) + return embedTTFType3(font, subset_index, charmap, descriptor) elif fonttype == 42: - return embedTTFType42(font, characters, descriptor) + return embedTTFType42(font, subset_index, charmap, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" @@ -2273,31 +2174,22 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.check_gc(gc, gc._rgb) prev_font = None, None oldx, oldy = 0, 0 - unsupported_chars = [] self.file.output(Op.begin_text) - for font, fontsize, num, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, num) - fontname = font.fname - if not _font_supports_glyph(fonttype, num): - # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in - # Type 42) must be emitted separately (below). - unsupported_chars.append((font, fontsize, ox, oy, num)) - else: - self._setup_textpos(ox, oy, 0, oldx, oldy) - oldx, oldy = ox, oy - if (fontname, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname), fontsize, - Op.selectfont) - prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), - Op.show) + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + subset_index, subset_charcode = self.file._character_tracker.track_glyph( + font, ccode, glyph_index) + font_path = FontPath(font.fname, font.face_index) + self._setup_textpos(ox, oy, 0, oldx, oldy) + oldx, oldy = ox, oy + if (font_path, subset_index, fontsize) != prev_font: + self.file.output(self.file.fontName(font_path, subset_index), fontsize, + Op.selectfont) + prev_font = font_path, subset_index, fontsize + self.file.output(self._encode_glyphs([subset_charcode], fonttype), + Op.show) self.file.output(Op.end_text) - for font, fontsize, ox, oy, num in unsupported_chars: - self._draw_xobject_glyph( - font, fontsize, font.get_char_index(num), ox, oy) - # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: self.file.output(Op.gsave, ox, oy, width, height, @@ -2328,13 +2220,13 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # one single-character string, but later it may have longer # strings interspersed with kern amounts. oldfont, seq = None, [] - for x1, y1, dvifont, glyph, width in page.text: - if dvifont != oldfont: - pdfname = self.file.dviFontName(dvifont) - seq += [['font', pdfname, dvifont.size]] - oldfont = dvifont - seq += [['text', x1, y1, [bytes([glyph])], x1+width]] - self.file._character_tracker.track(dvifont, chr(glyph)) + for text in page.text: + if text.font != oldfont: + pdfname = self.file.dviFontName(text.font) + seq += [['font', pdfname, text.font.size]] + oldfont = text.font + seq += [['text', text.x, text.y, [bytes([text.glyph])], text.x+text.width]] + self.file._character_tracker.track_glyph(text.font, text.glyph, text.index) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one @@ -2390,10 +2282,14 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): [0, 0]], pathops) self.draw_path(boxgc, path, mytrans, gc._rgb) - def encode_string(self, s, fonttype): + def _encode_glyphs(self, subset, fonttype): if fonttype in (1, 3): - return s.encode('cp1252', 'replace') - return s.encode('utf-16be', 'replace') + return bytes(subset) + return b''.join(glyph.to_bytes(2, 'big') for glyph in subset) + + def encode_string(self, s, fonttype): + encoding = {1: 'cp1252', 3: 'latin-1', 42: 'utf-16be'}[fonttype] + return s.encode(encoding, 'replace') def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -2405,62 +2301,48 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): return self.draw_mathtext(gc, x, y, s, prop, angle) fontsize = prop.get_size_in_points() + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None + # For Type-1 fonts, emit the whole string at once without manual kerning. if mpl.rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) - fonttype = 1 - else: - font = self._get_font_ttf(prop) - self.file._character_tracker.track(font, s) - fonttype = mpl.rcParams['pdf.fonttype'] - - if gc.get_url() is not None: - font.set_text(s) - width, height = font.get_width_height() - self.file._annotations[-1][1].append(_get_link_annotation( - gc, x, y, width / 64, height / 64, angle)) - - # If fonttype is neither 3 nor 42, emit the whole string at once - # without manual kerning. - if fonttype not in [3, 42]: self.file.output(Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) - self.file.output(self.encode_string(s, fonttype), + self.file.output(self.encode_string(s, fonttype=1), Op.show, Op.end_text) # A sequence of characters is broken into multiple chunks. The chunking # serves two purposes: - # - For Type 3 fonts, there is no way to access multibyte characters, - # as they cannot have a CIDMap. Therefore, in this case we break - # the string into chunks, where each chunk contains either a string - # of consecutive 1-byte characters or a single multibyte character. - # - A sequence of 1-byte characters is split into chunks to allow for - # kerning adjustments between consecutive chunks. + # - For Type 3 fonts, there is no way to access multibyte characters, as they + # cannot have a CIDMap. Therefore, in this case we break the string into + # chunks, where each chunk contains a string of consecutive 1-byte + # characters in a 256-character subset of the font. A distinct version of + # the original font is created for each 256-character subset. + # - A sequence of characters is split into chunks to allow for kerning + # adjustments between consecutive chunks. # - # Each chunk is emitted with a separate command: 1-byte characters use - # the regular text show command (TJ) with appropriate kerning between - # chunks, whereas multibyte characters use the XObject command (Do). + # Each chunk is emitted with the regular text show command (TJ) with appropriate + # kerning between chunks. else: - # List of (ft_object, start_x, [prev_kern, char, char, ...]), - # w/o zero kerns. - singlebyte_chunks = [] - # List of (ft_object, start_x, glyph_index). - multibyte_glyphs = [] - prev_was_multibyte = True - prev_font = font - for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): - if _font_supports_glyph(fonttype, ord(item.char)): - if prev_was_multibyte or item.ft_object != prev_font: - singlebyte_chunks.append((item.ft_object, item.x, [])) - prev_font = item.ft_object - if item.prev_kern: - singlebyte_chunks[-1][2].append(item.prev_kern) - singlebyte_chunks[-1][2].append(item.char) - prev_was_multibyte = False - else: - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_idx)) - prev_was_multibyte = True + font = self._get_font_ttf(prop) + fonttype = mpl.rcParams['pdf.fonttype'] + + def output_singlebyte_chunk(kerns_or_chars): + if not kerns_or_chars: + return + self.file.output( + # See pdf spec "Text space details" for the 1000/fontsize + # (aka. 1000/T_fs) factor. + [(-1000 * next(group) / fontsize) if tp == float # a kern + else self._encode_glyphs(group, fonttype) + for tp, group in itertools.groupby(kerns_or_chars, type)], + Op.showkern) + kerns_or_chars.clear() # Do the rotation and global translation as a single matrix # concatenation up front self.file.output(Op.gsave) @@ -2468,40 +2350,43 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.file.output(math.cos(a), math.sin(a), -math.sin(a), math.cos(a), x, y, Op.concat_matrix) - # Emit all the 1-byte characters in a BT/ET group. - - self.file.output(Op.begin_text) + # List of [prev_kern, char, char, ...] w/o zero kerns. + singlebyte_chunk = [] + prev_font = None prev_start_x = 0 - for ft_object, start_x, kerns_or_chars in singlebyte_chunks: - ft_name = self.file.fontName(ft_object.fname) - self.file.output(ft_name, fontsize, Op.selectfont) - self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0) - self.file.output( - # See pdf spec "Text space details" for the 1000/fontsize - # (aka. 1000/T_fs) factor. - [-1000 * next(group) / fontsize if tp == float # a kern - else self.encode_string("".join(group), fonttype) - for tp, group in itertools.groupby(kerns_or_chars, type)], - Op.showkern) - prev_start_x = start_x + # Emit all the characters in a BT/ET group. + self.file.output(Op.begin_text) + for item in _text_helpers.layout(s, font, features=features, + language=language): + subset, charcode = self.file._character_tracker.track_glyph( + item.ft_object, item.char, item.glyph_index) + if (item.ft_object, subset) != prev_font: + output_singlebyte_chunk(singlebyte_chunk) + font_path = FontPath(item.ft_object.fname, + item.ft_object.face_index) + ft_name = self.file.fontName(font_path, subset) + self.file.output(ft_name, fontsize, Op.selectfont) + self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) + prev_font = (item.ft_object, subset) + prev_start_x = item.x + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(item.y, Op.textrise) + if item.prev_kern: + singlebyte_chunk.append(item.prev_kern) + singlebyte_chunk.append(charcode) + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(0, Op.textrise) + output_singlebyte_chunk(singlebyte_chunk) self.file.output(Op.end_text) - # Then emit all the multibyte characters, one at a time. - for ft_object, start_x, glyph_idx in multibyte_glyphs: - self._draw_xobject_glyph( - ft_object, fontsize, glyph_idx, start_x, 0 - ) self.file.output(Op.grestore) - def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): - """Draw a multibyte character from a Type 3 font as an XObject.""" - glyph_name = font.get_glyph_name(glyph_idx) - name = self.file._get_xobject_glyph_name(font.fname, glyph_name) - self.file.output( - Op.gsave, - 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, - Name(name), Op.use_xobject, - Op.grestore, - ) + if gc.get_url() is not None: + font.set_text(s, features=features, language=language) + width, height = font.get_width_height() + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, width / 64, height / 64, angle)) def new_gc(self): # docstring inherited diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 2d2e24c3286c..3205f294ab2d 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -38,9 +38,17 @@ def _get_preamble(): """Prepare a LaTeX preamble based on the rcParams configuration.""" - font_size_pt = FontProperties( - size=mpl.rcParams["font.size"] - ).get_size_in_points() + def _to_fontspec(): + for command, family in [("setmainfont", "serif"), + ("setsansfont", "sans\\-serif"), + ("setmonofont", "monospace")]: + font_path = fm.findfont(family) + path = pathlib.Path(font_path) + yield r" \%s{%s}[Path=\detokenize{%s/}%s]" % ( + command, path.name, path.parent.as_posix(), + f',FontIndex={font_path.face_index:d}' if path.suffix == '.ttc' else '') + + font_size_pt = FontProperties(size=mpl.rcParams["font.size"]).get_size_in_points() return "\n".join([ # Remove Matplotlib's custom command \mathdefault. (Not using # \mathnormal instead since this looks odd with Computer Modern.) @@ -63,15 +71,8 @@ def _get_preamble(): *([ r"\ifdefined\pdftexversion\else % non-pdftex case.", r" \usepackage{fontspec}", - ] + [ - r" \%s{%s}[Path=\detokenize{%s/}]" - % (command, path.name, path.parent.as_posix()) - for command, path in zip( - ["setmainfont", "setsansfont", "setmonofont"], - [pathlib.Path(fm.findfont(family)) - for family in ["serif", "sans\\-serif", "monospace"]] - ) - ] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), + *_to_fontspec(), + r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), # Documented as "must come last". mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"), ]) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index b7fbbf59d778..f4d29ecfe347 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -88,16 +88,18 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, chars): +def _font_to_ps_type3(font_path, subset_index, glyph_indices): """ - Subset *chars* from the font at *font_path* into a Type 3 font. + Subset *glyphs_indices* from the font at *font_path* into a Type 3 font. Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + subset_index : int + The subset of the above font being created. + glyph_indices : set[int] + The glyphs to include in the subsetted font. Returns ------- @@ -106,13 +108,12 @@ def _font_to_ps_type3(font_path, chars): verbatim into a PostScript file. """ font = get_font(font_path, hinting_factor=1) - glyph_ids = [font.get_char_index(c) for c in chars] preamble = """\ %!PS-Adobe-3.0 Resource-Font %%Creator: Converted from TrueType to Type 3 by Matplotlib. 10 dict begin -/FontName /{font_name} def +/FontName /{font_name}-{subset} def /PaintType 0 def /FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def /FontBBox [{bbox}] def @@ -120,12 +121,12 @@ def _font_to_ps_type3(font_path, chars): /Encoding [{encoding}] def /CharStrings {num_glyphs} dict dup begin /.notdef 0 def -""".format(font_name=font.postscript_name, +""".format(font_name=font.postscript_name, subset=subset_index, inv_units_per_em=1 / font.units_per_EM, bbox=" ".join(map(str, font.bbox)), - encoding=" ".join(f"/{font.get_glyph_name(glyph_id)}" - for glyph_id in glyph_ids), - num_glyphs=len(glyph_ids) + 1) + encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}" + for glyph_index in glyph_indices), + num_glyphs=len(glyph_indices) + 1) postamble = """ end readonly def @@ -146,12 +147,12 @@ def _font_to_ps_type3(font_path, chars): """ entries = [] - for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { - "name": font.get_glyph_name(glyph_id), + "name": font.get_glyph_name(glyph_index), "bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])), } + _path.convert_to_string( @@ -169,35 +170,32 @@ def _font_to_ps_type3(font_path, chars): return preamble + "\n".join(entries) + postamble -def _font_to_ps_type42(font_path, chars, fh): +def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): """ - Subset *chars* from the font at *font_path* into a Type 42 font at *fh*. + Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*. Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + subset_index : int + The subset of the above font being created. + glyph_indices : set[int] + The glyphs to include in the subsetted font. fh : file-like Where to write the font. """ - subset_str = ''.join(chr(c) for c in chars) - _log.debug("SUBSET %s characters: %s", font_path, subset_str) + _log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices) try: - kw = {} - # fix this once we support loading more fonts from a collection - # https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541 - if font_path.endswith('.ttc'): - kw['fontNumber'] = 0 - with (fontTools.ttLib.TTFont(font_path, **kw) as font, - _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset): + with (fontTools.ttLib.TTFont(font_path.path, + fontNumber=font_path.face_index) as font, + _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( - "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, - len(fontdata) + "SUBSET %s:%d %d -> %d", font_path, subset_index, + os.stat(font_path).st_size, len(fontdata) ) - fh.write(_serialize_type42(font, subset, fontdata)) + fh.write(_serialize_type42(font, subset_index, subset, fontdata)) except RuntimeError: _log.warning( "The PostScript backend does not currently support the selected font (%s).", @@ -205,7 +203,7 @@ def _font_to_ps_type42(font_path, chars, fh): raise -def _serialize_type42(font, subset, fontdata): +def _serialize_type42(font, subset_index, subset, fontdata): """ Output a PostScript Type-42 format representation of font @@ -213,6 +211,8 @@ def _serialize_type42(font, subset, fontdata): ---------- font : fontTools.ttLib.ttFont.TTFont The original font object + subset_index : int + The subset of the above font to be created. subset : fontTools.ttLib.ttFont.TTFont The subset font object fontdata : bytes @@ -233,7 +233,7 @@ def _serialize_type42(font, subset, fontdata): 10 dict begin /FontType 42 def /FontMatrix [1 0 0 1 0 0] def - /FontName /{name.getDebugName(6)} def + /FontName /{name.getDebugName(6)}-{subset_index} def /FontInfo 7 dict dup begin /FullName ({name.getDebugName(4)}) def /FamilyName ({name.getDebugName(1)}) def @@ -427,7 +427,8 @@ def __init__(self, width, height, pswriter, imagedpi=72): self._clip_paths = {} self._path_collection_id = 0 - self._character_tracker = _backend_pdf_ps.CharacterTracker() + self._character_tracker = _backend_pdf_ps.CharacterTracker( + _backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0)) self._logwarn_once = functools.cache(_log.warning) def _is_transparent(self, rgb_or_rgba): @@ -771,15 +772,14 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) - stream = [] # list of (ps_name, x, char_name) + stream = [] # list of (ps_name, x, y, char_name) if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) - ps_name = (font.postscript_name.encode("ascii", "replace") - .decode("ascii")) + ps_name = font.postscript_name.encode("ascii", "replace").decode("ascii") scale = 0.001 * prop.get_size_in_points() thisx = 0 - last_name = None # kerns returns 0 for None. + last_name = '' # kerns returns 0 for ''. for c in s: name = uni2type1.get(ord(c), f"uni{ord(c):04X}") try: @@ -790,24 +790,33 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): kern = font.get_kern_dist_from_name(last_name, name) last_name = name thisx += kern * scale - stream.append((ps_name, thisx, name)) + stream.append((ps_name, thisx, 0, name)) thisx += width * scale else: + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None font = self._get_font_ttf(prop) - self._character_tracker.track(font, s) - for item in _text_helpers.layout(s, font): + for item in _text_helpers.layout(s, font, features=features, + language=language): + # NOTE: We ignore the character code in the subset, because PS uses the + # glyph name to write text. The subset is only used to ensure that each + # one does not overflow format limits. + subset, _ = self._character_tracker.track_glyph( + item.ft_object, item.char, item.glyph_index) ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) - glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) - stream.append((ps_name, item.x, glyph_name)) + glyph_name = item.ft_object.get_glyph_name(item.glyph_index) + stream.append((f'{ps_name}-{subset}', item.x, item.y, glyph_name)) self.set_color(*gc.get_rgb()) - for ps_name, group in itertools. \ - groupby(stream, lambda entry: entry[0]): + for ps_name, group in itertools.groupby(stream, lambda entry: entry[0]): self.set_font(ps_name, prop.get_size_in_points(), False) - thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow" - for _, x, name in group) + thetext = "\n".join(f"{x:g} {y:g} m /{name:s} glyphshow" + for _, x, y, name in group) self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} @@ -828,13 +837,17 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{x:g} {y:g} translate\n" f"{angle:g} rotate\n") lastfont = None - for font, fontsize, num, ox, oy in glyphs: - self._character_tracker.track_glyph(font, num) - if (font.postscript_name, fontsize) != lastfont: - lastfont = font.postscript_name, fontsize + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + # NOTE: We ignore the character code in the subset, because PS uses the + # glyph name to write text. The subset is only used to ensure that each one + # does not overflow format limits. + subset, _ = self._character_tracker.track_glyph( + font, ccode, glyph_index) + if (font.postscript_name, subset, fontsize) != lastfont: + lastfont = font.postscript_name, subset, fontsize self._pswriter.write( - f"/{font.postscript_name} {fontsize} selectfont\n") - glyph_name = font.get_glyph_name(font.get_char_index(num)) + f"/{font.postscript_name}-{subset} {fontsize} selectfont\n") + glyph_name = font.get_glyph_name(glyph_index) self._pswriter.write( f"{ox:g} {oy:g} moveto\n" f"/{glyph_name} glyphshow\n") @@ -1067,24 +1080,21 @@ def print_figure_impl(fh): Ndict = len(_psDefs) print("%%BeginProlog", file=fh) if not mpl.rcParams['ps.useafm']: - Ndict += len(ps_renderer._character_tracker.used) + Ndict += sum(map(len, ps_renderer._character_tracker.used.values())) print("/mpldict %d dict def" % Ndict, file=fh) print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for font_path, chars \ - in ps_renderer._character_tracker.used.items(): - if not chars: - continue - fonttype = mpl.rcParams['ps.fonttype'] - # Can't use more than 255 chars from a single Type 3 font. - if len(chars) > 255: - fonttype = 42 - fh.flush() - if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, chars)) - else: # Type 42 only. - _font_to_ps_type42(font_path, chars, fh) + for font, subsets in ps_renderer._character_tracker.used.items(): + for subset, charmap in enumerate(subsets): + if not charmap: + continue + fonttype = mpl.rcParams['ps.fonttype'] + fh.flush() + if fonttype == 3: + fh.write(_font_to_ps_type3(font, subset, charmap.values())) + else: # Type 42 only. + _font_to_ps_type42(font, subset, charmap.values(), fh) print("end", file=fh) print("%%EndProlog", file=fh) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index d46dbba80a3c..6445915de38b 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1029,19 +1029,19 @@ def _update_glyph_map_defs(self, glyph_map_new): writer = self.writer if glyph_map_new: writer.start('defs') - for char_id, (vertices, codes) in glyph_map_new.items(): - char_id = self._adjust_char_id(char_id) + for glyph_repr, (vertices, codes) in glyph_map_new.items(): + glyph_repr = self._adjust_glyph_repr(glyph_repr) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( Path(vertices * 64, codes), simplify=False) writer.element( - 'path', id=char_id, d=path_data, + 'path', id=glyph_repr, d=path_data, transform=_generate_transform([('scale', (1 / 64,))])) writer.end('defs') self._glyph_map.update(glyph_map_new) - def _adjust_char_id(self, char_id): - return char_id.replace("%20", "_") + def _adjust_glyph_repr(self, glyph_repr): + return glyph_repr.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # docstring inherited @@ -1054,6 +1054,11 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None style = {} if color != '#000000': @@ -1074,19 +1079,19 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) - _glyphs = text2path.get_glyphs_with_font( - font, s, glyph_map=glyph_map, return_new_glyphs_only=True) - glyph_info, glyph_map_new, rects = _glyphs + glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( + font, s, features=features, language=language, + glyph_map=glyph_map, return_new_glyphs_only=True) self._update_glyph_map_defs(glyph_map_new) - for glyph_id, xposition, yposition, scale in glyph_info: + for glyph_repr, xposition, yposition, scale in glyph_info: writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{glyph_id}'}) + attrib={'xlink:href': f'#{glyph_repr}'}) else: if ismath == "TeX": @@ -1098,15 +1103,15 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) - for char_id, xposition, yposition, scale in glyph_info: - char_id = self._adjust_char_id(char_id) + for glyph_repr, xposition, yposition, scale in glyph_info: + glyph_repr = self._adjust_glyph_repr(glyph_repr) writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{char_id}'}) + attrib={'xlink:href': f'#{glyph_repr}'}) for verts, codes in rects: path = Path(verts, codes) @@ -1231,7 +1236,7 @@ def _get_all_quoted_names(prop): # Sort the characters by font, and output one tspan for each. spans = {} - for font, fontsize, thetext, new_x, new_y in glyphs: + for font, fontsize, ccode, glyph_index, new_x, new_y in glyphs: entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes @@ -1246,9 +1251,9 @@ def _get_all_quoted_names(prop): if entry.stretch != 'normal': font_style['font-stretch'] = entry.stretch style = _generate_css({**font_style, **color_style}) - if thetext == 32: - thetext = 0xa0 # non-breaking space - spans.setdefault(style, []).append((new_x, -new_y, thetext)) + if ccode == 32: + ccode = 0xa0 # non-breaking space + spans.setdefault(style, []).append((new_x, -new_y, ccode)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 21fe881bcc7b..dd5efdc85b34 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -719,6 +719,10 @@ def fname(self): """A fake filename""" return self.texname.decode('latin-1') + @property + def face_index(self): # For compatibility with FT2Font. + return 0 + def _get_fontmap(self, string): """Get the mapping from characters to the font that includes them. diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 1c24ff1c28a9..de429bd0b7f1 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -8,6 +8,9 @@ from collections.abc import Generator from typing import NamedTuple from typing import Self +from .ft2font import CharacterCodeType, GlyphIndexType + + class _dvistate(Enum): pre = ... outer = ... @@ -32,7 +35,7 @@ class Text(NamedTuple): x: int y: int font: DviFont - glyph: int + glyph: CharacterCodeType width: int @property def font_path(self) -> Path: ... @@ -41,9 +44,9 @@ class Text(NamedTuple): @property def font_effects(self) -> dict[str, float]: ... @property - def index(self) -> int: ... # type: ignore[override] + def index(self) -> GlyphIndexType: ... # type: ignore[override] @property - def glyph_name_or_index(self) -> int | str: ... + def glyph_name_or_index(self) -> GlyphIndexType | str: ... class Dvi: file: io.BufferedReader @@ -75,6 +78,8 @@ class DviFont: def widths(self) -> list[int]: ... @property def fname(self) -> str: ... + @property + def face_index(self) -> int: ... def resolve_path(self) -> Path: ... @property def subfont(self) -> int: ... diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 0a5588d13452..90aa778cb292 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -28,7 +28,6 @@ from __future__ import annotations from base64 import b64encode -import copy import dataclasses from functools import cache, lru_cache import functools @@ -313,6 +312,69 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): return [fname for fname in fontfiles if os.path.exists(fname)] +# To maintain backwards-compatibility with the current code we need to continue to +# return a str. However to support indexing into the file we need to return both the +# path and the index. Thus, we sub-class str to maintain compatibility and extend it to +# carry the index. +# +# The other alternative would be to create a completely new API and deprecate the +# existing one. In this case, sub-classing str is the simpler and less-disruptive +# option. +class FontPath(str): + """ + A class to describe a path to a font with a face index. + + Parameters + ---------- + path : str + The path to a font. + face_index : int + The face index in the font. + """ + + __match_args__ = ('path', 'face_index') + + def __new__(cls, path, face_index): + ret = super().__new__(cls, path) + ret._face_index = face_index + return ret + + @property + def path(self): + """The path to a font.""" + return str(self) + + @property + def face_index(self): + """The face index in a font.""" + return self._face_index + + def _as_tuple(self): + return (self.path, self.face_index) + + def __eq__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() == other._as_tuple() + return super().__eq__(other) + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() < other._as_tuple() + return super().__lt__(other) + + def __gt__(self, other): + return not (self == other or self < other) + + def __hash__(self): + return hash(self._as_tuple()) + + def __repr__(self): + return f'FontPath{self._as_tuple()}' + + @dataclasses.dataclass(frozen=True) class FontEntry: """ @@ -322,6 +384,7 @@ class FontEntry: """ fname: str = '' + index: int = 0 name: str = '' style: str = 'normal' variant: str = 'normal' @@ -468,7 +531,84 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. raise NotImplementedError("Non-scalable fonts are not supported") size = 'scalable' - return FontEntry(font.fname, name, style, variant, weight, stretch, size) + return FontEntry(font.fname, font.face_index, name, + style, variant, weight, stretch, size) + + +def _get_font_alt_names(font, primary_name): + """ + Return ``(name, weight)`` pairs for alternate family names of *font*. + + A font file can advertise its family name in several places. FreeType + exposes ``font.family_name``, which is typically derived from the + Macintosh-platform Name ID 1 entry. However, other entries may carry + different (equally valid) names that users reasonably expect to work: + + - **Name ID 1, other platform** — some fonts store a different family name + on the Microsoft platform than on the Macintosh platform. + - **Name ID 16** — "Typographic Family" (a.k.a. preferred family): groups + more than the traditional four styles under one name. + - **Name ID 21** — "WWS Family": an even narrower grouping used by some + fonts (weight/width/slope only). + + Each name is paired with a weight derived from the corresponding subfamily + entry on the *same* platform. This ensures that the weight of the alternate entry + reflects the font's role *within that named family* rather than its absolute + typographic weight. + + Parameters + ---------- + font : `.FT2Font` + primary_name : str + The family name already extracted from the font (``font.family_name``). + + Returns + ------- + list of (str, int) + ``(alternate_family_name, weight)`` pairs, not including *primary_name*. + """ + try: + sfnt = font.get_sfnt() + except ValueError: + return [] + + mac_key = (1, # platform: macintosh + 0, # id: roman + 0) # langid: english + ms_key = (3, # platform: microsoft + 1, # id: unicode_cs + 0x0409) # langid: english_united_states + + seen = {primary_name} + result = [] + + def _weight_from_subfam(subfam): + subfam = subfam.replace(" ", "") + for regex, weight in _weight_regexes: + if re.search(regex, subfam, re.I): + return weight + return 400 # "Regular" or unrecognised + + def _try_add(name, subfam): + name = name.strip() + if not name or name in seen: + return + seen.add(name) + result.append((name, _weight_from_subfam(subfam.strip()))) + + # Each family-name ID is paired with its corresponding subfamily ID on the + # same platform: (family_id, subfamily_id). + for fam_id, subfam_id in ((1, 2), (16, 17), (21, 22)): + _try_add( + sfnt.get((*mac_key, fam_id), b'').decode('latin-1'), + sfnt.get((*mac_key, subfam_id), b'').decode('latin-1'), + ) + _try_add( + sfnt.get((*ms_key, fam_id), b'').decode('utf-16-be'), + sfnt.get((*ms_key, subfam_id), b'').decode('utf-16-be'), + ) + + return result def afmFontProperty(fontpath, font): @@ -538,12 +678,12 @@ def afmFontProperty(fontpath, font): size = 'scalable' - return FontEntry(fontpath, name, style, variant, weight, stretch, size) + return FontEntry(fontpath, 0, name, style, variant, weight, stretch, size) def _cleanup_fontproperties_init(init_method): """ - A decorator to limit the call signature to single a positional argument + A decorator to limit the call signature to a single positional argument or alternatively only keyword arguments. We still accept but deprecate all other call signatures. @@ -705,15 +845,7 @@ def _from_any(cls, arg): return cls(**arg) def __hash__(self): - l = (tuple(self.get_family()), - self.get_slant(), - self.get_variant(), - self.get_weight(), - self.get_stretch(), - self.get_size(), - self.get_file(), - self.get_math_fontfamily()) - return hash(l) + return hash(tuple(self.__dict__.values())) def __eq__(self, other): return hash(self) == hash(other) @@ -729,7 +861,7 @@ def get_family(self): from their respective rcParams when searching for a matching font) in the order of preference. """ - return self._family + return list(self._family) def get_name(self): """ @@ -798,8 +930,8 @@ def set_family(self, family): """ family = mpl._val_or_rc(family, 'font.family') if isinstance(family, str): - family = [family] - self._family = family + family = (family,) + self._family = tuple(family) def set_style(self, style): """ @@ -959,9 +1091,15 @@ def set_math_fontfamily(self, fontfamily): _api.check_in_list(valid_fonts, math_fontfamily=fontfamily) self._math_fontfamily = fontfamily + def __copy__(self): + # Bypass __init__ for speed, since values are already validated + new = FontProperties.__new__(FontProperties) + new.__dict__.update(self.__dict__) + return new + def copy(self): """Return a copy of self.""" - return copy.copy(self) + return self.__copy__() # Aliases set_name = set_family @@ -1072,7 +1210,7 @@ class FontManager: # Increment this version number whenever the font cache data # format or behavior has changed and requires an existing font # cache files to be rebuilt. - __version__ = '3.11.0a1' + __version__ = '3.11.0a4' def __init__(self, size=None, weight='normal'): self._version = self.__version__ @@ -1141,6 +1279,18 @@ def addfont(self, path): font = ft2font.FT2Font(path) prop = ttfFontProperty(font) self.ttflist.append(prop) + for alt_name, alt_weight in _get_font_alt_names(font, prop.name): + self.ttflist.append( + dataclasses.replace(prop, name=alt_name, weight=alt_weight)) + + for face_index in range(1, font.num_faces): + subfont = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(subfont) + self.ttflist.append(prop) + for alt_name, alt_weight in _get_font_alt_names(subfont, prop.name): + self.ttflist.append( + dataclasses.replace(prop, name=alt_name, weight=alt_weight)) + self._findfont_cached.cache_clear() @property @@ -1327,7 +1477,7 @@ def findfont(self, prop, fontext='ttf', directory=None, Returns ------- - str + FontPath The filename of the best matching font. Notes @@ -1399,7 +1549,7 @@ def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, Returns ------- - list[str] + list[FontPath] The paths of the fonts found. Notes @@ -1545,10 +1695,10 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # actually raised. return cbook._ExceptionInfo(ValueError, "No valid font could be found") - return _cached_realpath(result) + return FontPath(_cached_realpath(result), best_font.index) -@lru_cache +@_api.deprecated("3.11") def is_opentype_cff_font(filename): """ Return whether the given font is a Postscript Compact Font Format Font @@ -1565,15 +1715,16 @@ def is_opentype_cff_font(filename): @lru_cache(64) def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, enable_last_resort): - first_fontpath, *rest = font_filepaths + (first_fontpath, first_fontindex), *rest = font_filepaths fallback_list = [ - ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor) - for fpath in rest + ft2font.FT2Font(fpath, hinting_factor, face_index=index, + _kerning_factor=_kerning_factor) + for fpath, index in rest ] last_resort_path = _cached_realpath( cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf')) try: - last_resort_index = font_filepaths.index(last_resort_path) + last_resort_index = font_filepaths.index((last_resort_path, 0)) except ValueError: last_resort_index = -1 # Add Last Resort font so we always have glyphs regardless of font, unless we're @@ -1585,7 +1736,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, _warn_if_used=True)) last_resort_index = len(fallback_list) font = ft2font.FT2Font( - first_fontpath, hinting_factor, + first_fontpath, hinting_factor, face_index=first_fontindex, _fallback_list=fallback_list, _kerning_factor=_kerning_factor ) @@ -1620,10 +1771,11 @@ def get_font(font_filepaths, hinting_factor=None): Parameters ---------- - font_filepaths : Iterable[str, Path, bytes], str, Path, bytes + font_filepaths : Iterable[str, bytes, os.PathLike, FontPath], \ +str, bytes, os.PathLike, FontPath Relative or absolute paths to the font files to be used. - If a single string, bytes, or `pathlib.Path`, then it will be treated + If a single string, bytes, or `os.PathLike`, then it will be treated as a list with that entry only. If more than one filepath is passed, then the returned FT2Font object @@ -1635,14 +1787,20 @@ def get_font(font_filepaths, hinting_factor=None): `.ft2font.FT2Font` """ - if isinstance(font_filepaths, (str, Path, bytes)): - paths = (_cached_realpath(font_filepaths),) - else: - paths = tuple(_cached_realpath(fname) for fname in font_filepaths) + match font_filepaths: + case FontPath(path, index): + paths = ((_cached_realpath(path), index), ) + case str() | bytes() | os.PathLike() as path: + paths = ((_cached_realpath(path), 0), ) + case _: + paths = tuple( + (_cached_realpath(fname.path), fname.face_index) + if isinstance(fname, FontPath) else (_cached_realpath(fname), 0) + for fname in font_filepaths) hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor') - return _get_font( + font = _get_font( # must be a tuple to be cached paths, hinting_factor, @@ -1651,6 +1809,10 @@ def get_font(font_filepaths, hinting_factor=None): thread_id=threading.get_ident(), enable_last_resort=mpl.rcParams['font.enable_last_resort'], ) + # Ensure the transform is always consistent. + font._set_transform([[round(0x10000 / font._hinting_factor), 0], [0, 0x10000]], + [0, 0]) + return font def _load_fontmanager(*, try_read_cache=True): diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index e865f67384cd..22d925ea9273 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -3,7 +3,7 @@ from dataclasses import dataclass from numbers import Integral import os from pathlib import Path -from typing import Any, Literal +from typing import Any, Final, Literal from matplotlib._afm import AFM from matplotlib import ft2font @@ -23,12 +23,32 @@ def get_fontext_synonyms(fontext: str) -> list[str]: ... def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ... def win32FontDirectory() -> str: ... def _get_fontconfig_fonts() -> list[Path]: ... +def _get_font_alt_names( + font: ft2font.FT2Font, primary_name: str +) -> list[tuple[str, int]]: ... def findSystemFonts( - fontpaths: Iterable[str | os.PathLike | Path] | None = ..., fontext: str = ... + fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ... ) -> list[str]: ... + +class FontPath(str): + __match_args__: Final[tuple[str, ...]] + def __new__(cls: type[str], path: str, face_index: int) -> FontPath: ... + @property + def path(self) -> str: ... + @property + def face_index(self) -> int: ... + def _as_tuple(self) -> tuple[str, int]: ... + def __eq__(self, other: Any) -> bool: ... + def __ne__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + def __gt__(self, other: Any) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + @dataclass class FontEntry: fname: str = ... + index: int = ... name: str = ... style: str = ... variant: str = ... @@ -50,10 +70,11 @@ class FontProperties: weight: int | str | None = ..., stretch: int | str | None = ..., size: float | str | None = ..., - fname: str | os.PathLike | Path | None = ..., + fname: str | os.PathLike | None = ..., math_fontfamily: str | None = ..., ) -> None: ... def __hash__(self) -> int: ... + def __copy__(self) -> FontProperties: ... def __eq__(self, other: object) -> bool: ... def get_family(self) -> list[str]: ... def get_name(self) -> str: ... @@ -72,7 +93,7 @@ class FontProperties: def set_weight(self, weight: int | str | None) -> None: ... def set_stretch(self, stretch: int | str | None) -> None: ... def set_size(self, size: float | str | None) -> None: ... - def set_file(self, file: str | os.PathLike | Path | None) -> None: ... + def set_file(self, file: str | os.PathLike | None) -> None: ... def set_fontconfig_pattern(self, pattern: str) -> None: ... def get_math_fontfamily(self) -> str: ... def set_math_fontfamily(self, fontfamily: str | None) -> None: ... @@ -83,8 +104,8 @@ class FontProperties: set_slant = set_style get_size_in_points = get_size -def json_dump(data: FontManager, filename: str | Path | os.PathLike) -> None: ... -def json_load(filename: str | Path | os.PathLike) -> FontManager: ... +def json_dump(data: FontManager, filename: str | os.PathLike) -> None: ... +def json_load(filename: str | os.PathLike) -> FontManager: ... class FontManager: __version__: str @@ -93,7 +114,7 @@ class FontManager: afmlist: list[FontEntry] ttflist: list[FontEntry] def __init__(self, size: float | None = ..., weight: str = ...) -> None: ... - def addfont(self, path: str | Path | os.PathLike) -> None: ... + def addfont(self, path: str | os.PathLike) -> None: ... @property def defaultFont(self) -> dict[str, str]: ... def get_default_weight(self) -> str: ... @@ -115,12 +136,12 @@ class FontManager: directory: str | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., - ) -> str: ... + ) -> FontPath: ... def get_font_names(self) -> list[str]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( - font_filepaths: Iterable[str | Path | bytes] | str | Path | bytes, + font_filepaths: Iterable[str | bytes | os.PathLike | FontPath] | str | bytes | os.PathLike | FontPath, hinting_factor: int | None = ..., ) -> ft2font.FT2Font: ... diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 3da01245e648..3003f83932bc 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,6 +1,7 @@ from enum import Enum, Flag +from os import PathLike import sys -from typing import BinaryIO, Literal, TypedDict, final, overload, cast +from typing import BinaryIO, Literal, NewType, NotRequired, TypeAlias, TypedDict, cast, final, overload from typing_extensions import Buffer # < Py 3.12 import numpy as np @@ -8,6 +9,13 @@ from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str +__libraqm_version__: str + +# We can't change the type hints for standard library chr/ord, so character codes are a +# simple type alias. +CharacterCodeType: TypeAlias = int +# But glyph indices are internal, so use a distinct type hint. +GlyphIndexType = NewType('GlyphIndexType', int) class FaceFlags(Flag): SCALABLE = cast(int, ...) @@ -62,6 +70,14 @@ class LoadFlags(Flag): TARGET_LCD = cast(int, ...) TARGET_LCD_V = cast(int, ...) +class RenderMode(Enum): + NORMAL = cast(int, ...) + LIGHT = cast(int, ...) + MONO = cast(int, ...) + LCD = cast(int, ...) + LCD_V = cast(int, ...) + SDF = cast(int, ...) + class StyleFlags(Flag): NORMAL = cast(int, ...) ITALIC = cast(int, ...) @@ -121,11 +137,27 @@ class _SfntOs2Dict(TypedDict): yStrikeoutPosition: int sFamilyClass: int panose: bytes - ulCharRange: tuple[int, int, int, int] + ulUnicodeRange: tuple[int, int, int, int] achVendID: bytes fsSelection: int - fsFirstCharIndex: int - fsLastCharIndex: int + usFirstCharIndex: int + usLastCharIndex: int + sTypoAscender: int + sTypoDescender: int + sTypoLineGap: int + usWinAscent: int + usWinDescent: int + # version >= 1 + ulCodePageRange: NotRequired[tuple[int, int]] + # version >= 2 + sxHeight: NotRequired[int] + sCapHeight: NotRequired[int] + usDefaultChar: NotRequired[int] + usBreakChar: NotRequired[int] + usMaxContext: NotRequired[int] + # version >= 5 + usLowerOpticalPointSize: NotRequired[int] + usUpperOpticalPointSize: NotRequired[int] class _SfntHheaDict(TypedDict): version: tuple[int, int] @@ -149,7 +181,7 @@ class _SfntVheaDict(TypedDict): vertTypoLineGap: int advanceHeightMax: int minTopSideBearing: int - minBottomSizeBearing: int + minBottomSideBearing: int yMaxExtent: int caretSlopeRise: int caretSlopeRun: int @@ -183,32 +215,55 @@ class _SfntPcltDict(TypedDict): widthType: int serifStyle: int +@final +class LayoutItem: + @property + def ft_object(self) -> FT2Font: ... + @property + def char(self) -> str: ... + @property + def glyph_index(self) -> GlyphIndexType: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def prev_kern(self) -> float: ... + def __str__(self) -> str: ... + @final class FT2Font(Buffer): def __init__( self, - filename: str | BinaryIO, + filename: str | bytes | PathLike | BinaryIO, hinting_factor: int = ..., *, + face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., - _kerning_factor: int = ... + _kerning_factor: int | None = ... ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, /, flags: int) -> memoryview: ... - def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... + def _layout( + self, + text: str, + flags: LoadFlags, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = ..., + ) -> list[LayoutItem]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ... ) -> None: ... def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ... def get_bitmap_offset(self) -> tuple[int, int]: ... - def get_char_index(self, codepoint: int) -> int: ... - def get_charmap(self) -> dict[int, int]: ... + def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ... + def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ... def get_descent(self) -> int: ... - def get_glyph_name(self, index: int) -> str: ... + def get_glyph_name(self, index: GlyphIndexType) -> str: ... def get_image(self) -> NDArray[np.uint8]: ... - def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ... - def get_name_index(self, name: str) -> int: ... + def get_kerning(self, left: GlyphIndexType, right: GlyphIndexType, mode: Kerning) -> int: ... + def get_name_index(self, name: str) -> GlyphIndexType: ... def get_num_glyphs(self) -> int: ... def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ... def get_ps_font_info( @@ -230,13 +285,19 @@ class FT2Font(Buffer): @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ... - def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ... + def load_char(self, charcode: CharacterCodeType, flags: LoadFlags = ...) -> Glyph: ... + def load_glyph(self, glyphindex: GlyphIndexType, flags: LoadFlags = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... def set_text( - self, string: str, angle: float = ..., flags: LoadFlags = ... + self, + string: str, + angle: float = ..., + flags: LoadFlags = ..., + *, + features: tuple[str] | None = ..., + language: str | list[tuple[str, int, int]] | None = ..., ) -> NDArray[np.float64]: ... @property def ascender(self) -> int: ... @@ -247,9 +308,11 @@ class FT2Font(Buffer): @property def face_flags(self) -> FaceFlags: ... @property + def face_index(self) -> int: ... + @property def family_name(self) -> str: ... @property - def fname(self) -> str: ... + def fname(self) -> str | bytes: ... @property def height(self) -> int: ... @property diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 9ce2228b5954..69ad36fb768b 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -684,7 +684,8 @@ def recache(self, always=False): y = self._y self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float) - self._x, self._y = self._xy.T # views + self._x = self._xy[:, 0] # views of the x and y data + self._y = self._xy[:, 1] self._subslice = False if (self.axes diff --git a/lib/matplotlib/mpl-data/fonts/afm/cmti10.afm b/lib/matplotlib/mpl-data/fonts/afm/cmti10.afm new file mode 100644 index 000000000000..ac9e89f676b8 --- /dev/null +++ b/lib/matplotlib/mpl-data/fonts/afm/cmti10.afm @@ -0,0 +1,333 @@ +StartFontMetrics 2.0 +FontName cmti10 +FullName cmti10 +FamilyName cmti10 +Weight Medium +ItalicAngle 0.000000 +IsFixedPitch false +UnderlinePosition -133 +UnderlineThickness 20 +Version 1.1/12-Nov-94 +FontBBox -35, -250, 1125, 750 +Notice Copyright \(C\) 1994, Basil K. Malyshev. All Rights Reserved.\nBaKoMa Fonts Collection, Level-B. +EncodingScheme FontSpecific +CapHeight 683 +XHeight 431 +Descender -194 +Ascender 694 +StartCharMetrics 129 +C 0 ; WX 627.22 ; N Gamma ; B 59 0 706 683 ; +C 1 ; WX 817.78 ; N Delta ; B 70 0 752 716 ; +C 2 ; WX 766.67 ; N Theta ; B 148 -22 788 705 ; +C 3 ; WX 692.22 ; N Lambda ; B 58 0 643 716 ; +C 4 ; WX 664.44 ; N Xi ; B 75 0 755 683 ; +C 5 ; WX 743.33 ; N Pi ; B 59 0 854 683 ; +C 6 ; WX 715.56 ; N Sigma ; B 80 0 782 683 ; +C 7 ; WX 766.67 ; N Upsilon ; B 213 0 833 705 ; +C 8 ; WX 715.56 ; N Phi ; B 158 0 729 683 ; +C 9 ; WX 766.67 ; N Psi ; B 211 0 825 683 ; +C 10 ; WX 715.56 ; N Omega ; B 100 0 759 705 ; +C 11 ; WX 613.33 ; N ff ; B -25 -205 758 705 ; L i ffi ; L l ffl ; +C 12 ; WX 562.22 ; N fi ; B -25 -205 597 705 ; +C 13 ; WX 587.78 ; N fl ; B -25 -205 639 705 ; +C 14 ; WX 881.67 ; N ffi ; B -25 -205 917 705 ; +C 15 ; WX 894.44 ; N ffl ; B -25 -205 945 705 ; +C 16 ; WX 306.67 ; N dotlessi ; B 81 -11 334 442 ; +C 17 ; WX 332.22 ; N dotlessj ; B -35 -205 322 442 ; +C 18 ; WX 511.11 ; N grave ; B 291 503 433 696 ; +C 19 ; WX 511.11 ; N acute ; B 339 503 551 696 ; +C 20 ; WX 511.11 ; N caron ; B 279 505 538 633 ; +C 21 ; WX 511.11 ; N breve ; B 280 521 567 694 ; +C 22 ; WX 511.11 ; N macron ; B 232 555 566 589 ; +C 23 ; WX 831.28 ; N ring ; B 476 541 670 716 ; +C 24 ; WX 460 ; N cedilla ; B 99 -194 338 0 ; +C 25 ; WX 536.67 ; N germandbls ; B -20 -205 578 705 ; +C 26 ; WX 715.56 ; N ae ; B 90 -11 721 442 ; +C 27 ; WX 715.56 ; N oe ; B 106 -15 721 446 ; +C 28 ; WX 511.11 ; N oslash ; B 66 -109 553 540 ; +C 29 ; WX 882.78 ; N AE ; B 59 0 949 683 ; +C 30 ; WX 985 ; N OE ; B 162 -22 1051 705 ; +C 31 ; WX 766.67 ; N Oslash ; B 120 -62 818 745 ; +C 32 ; WX 255.56 ; N polishlcross ; B 91 280 345 393 ; +C 33 ; WX 306.67 ; N exclam ; B 111 0 376 716 ; L quoteleft exclamdown ; +C 34 ; WX 514.44 ; N quotedblright ; B 176 390 520 694 ; +C 35 ; WX 817.78 ; N numbersign ; B 115 -194 828 694 ; +C 36 ; WX 769.11 ; N dollar ; B 88 -11 698 709 ; +C 37 ; WX 817.78 ; N percent ; B 145 -56 847 750 ; +C 38 ; WX 766.67 ; N ampersand ; B 127 -22 803 716 ; +C 39 ; WX 306.67 ; N quoteright ; B 218 390 375 694 ; L quoteright quotedblright ; +C 40 ; WX 408.89 ; N parenleft ; B 150 -250 517 750 ; +C 41 ; WX 408.89 ; N parenright ; B 17 -250 384 750 ; +C 42 ; WX 511.11 ; N asterisk ; B 195 319 584 750 ; +C 43 ; WX 766.67 ; N plus ; B 140 -57 753 557 ; +C 44 ; WX 306.67 ; N comma ; B 72 -194 226 110 ; +C 45 ; WX 357.78 ; N hyphen ; B 85 185 340 245 ; L hyphen endash ; +C 46 ; WX 306.67 ; N period ; B 111 0 224 110 ; +C 47 ; WX 511.11 ; N slash ; B 20 -250 617 750 ; +C 48 ; WX 511.11 ; N zero ; B 115 -22 556 666 ; +C 49 ; WX 511.11 ; N one ; B 115 0 463 666 ; +C 50 ; WX 511.11 ; N two ; B 82 -22 551 666 ; +C 51 ; WX 511.11 ; N three ; B 95 -22 562 666 ; +C 52 ; WX 511.11 ; N four ; B 44 -194 475 666 ; +C 53 ; WX 511.11 ; N five ; B 107 -22 567 666 ; +C 54 ; WX 511.11 ; N six ; B 120 -22 567 666 ; +C 55 ; WX 511.11 ; N seven ; B 142 -22 628 666 ; +C 56 ; WX 511.11 ; N eight ; B 97 -22 554 666 ; +C 57 ; WX 511.11 ; N nine ; B 105 -22 553 666 ; +C 58 ; WX 306.67 ; N colon ; B 111 0 305 431 ; +C 59 ; WX 306.67 ; N semicolon ; B 72 -194 305 431 ; +C 60 ; WX 306.67 ; N exclamdown ; B 57 -216 322 500 ; +C 61 ; WX 766.67 ; N equal ; B 115 133 776 367 ; +C 62 ; WX 511.11 ; N questiondown ; B 85 -216 442 500 ; +C 63 ; WX 511.11 ; N question ; B 194 0 551 716 ; L quoteleft questiondown ; +C 64 ; WX 766.67 ; N at ; B 151 -11 789 705 ; +C 65 ; WX 743.33 ; N A ; B 58 0 693 716 ; +C 66 ; WX 703.89 ; N B ; B 62 0 734 683 ; +C 67 ; WX 715.56 ; N C ; B 150 -22 813 705 ; +C 68 ; WX 755 ; N D ; B 60 0 775 683 ; +C 69 ; WX 678.33 ; N E ; B 59 0 744 683 ; +C 70 ; WX 652.78 ; N F ; B 59 0 732 683 ; +C 71 ; WX 773.61 ; N G ; B 150 -22 813 705 ; +C 72 ; WX 743.33 ; N H ; B 59 0 854 683 ; +C 73 ; WX 385.56 ; N I ; B 55 0 503 683 ; +C 74 ; WX 525 ; N J ; B 90 -22 622 683 ; +C 75 ; WX 768.89 ; N K ; B 59 0 860 683 ; +C 76 ; WX 627.22 ; N L ; B 59 0 626 683 ; +C 77 ; WX 896.67 ; N M ; B 63 0 1004 683 ; +C 78 ; WX 743.33 ; N N ; B 59 0 854 683 ; +C 79 ; WX 766.67 ; N O ; B 148 -22 788 705 ; +C 80 ; WX 678.33 ; N P ; B 60 0 730 683 ; +C 81 ; WX 766.67 ; N Q ; B 148 -194 788 705 ; +C 82 ; WX 729.44 ; N R ; B 60 -22 723 683 ; +C 83 ; WX 562.22 ; N S ; B 74 -22 633 705 ; +C 84 ; WX 715.56 ; N T ; B 175 0 808 683 ; +C 85 ; WX 743.33 ; N U ; B 200 -22 854 683 ; +C 86 ; WX 743.33 ; N V ; B 208 -22 868 683 ; +C 87 ; WX 998.89 ; N W ; B 207 -22 1125 683 ; +C 88 ; WX 743.33 ; N X ; B 50 0 825 683 ; +C 89 ; WX 743.33 ; N Y ; B 201 0 875 683 ; +C 90 ; WX 613.33 ; N Z ; B 79 0 704 683 ; +C 91 ; WX 306.67 ; N bracketleft ; B 73 -250 446 750 ; +C 92 ; WX 514.44 ; N quotedblleft ; B 265 390 609 694 ; +C 93 ; WX 306.67 ; N bracketright ; B -14 -250 359 750 ; +C 94 ; WX 511.11 ; N circumflex ; B 264 533 524 694 ; +C 95 ; WX 306.67 ; N dotaccent ; B 251 559 364 669 ; +C 96 ; WX 306.67 ; N quoteleft ; B 204 390 361 694 ; L quoteleft quotedblleft ; +C 97 ; WX 511.11 ; N a ; B 107 -11 538 442 ; +C 98 ; WX 460 ; N b ; B 113 -11 461 694 ; +C 99 ; WX 460 ; N c ; B 108 -11 470 442 ; +C 100 ; WX 511.11 ; N d ; B 107 -11 562 694 ; +C 101 ; WX 460 ; N e ; B 112 -11 468 442 ; +C 102 ; WX 306.67 ; N f ; B -25 -205 452 705 ; L i fi ; L f ff ; L l fl ; +C 103 ; WX 460 ; N g ; B 51 -205 489 442 ; +C 104 ; WX 511.11 ; N h ; B 74 -11 538 694 ; +C 105 ; WX 306.67 ; N i ; B 81 -11 334 656 ; +C 106 ; WX 306.67 ; N j ; B -35 -205 359 656 ; +C 107 ; WX 460 ; N k ; B 74 -11 501 694 ; +C 108 ; WX 255.56 ; N l ; B 92 -11 308 694 ; +C 109 ; WX 817.78 ; N m ; B 81 -11 845 442 ; +C 110 ; WX 562.22 ; N n ; B 81 -11 589 442 ; +C 111 ; WX 511.11 ; N o ; B 108 -11 511 442 ; +C 112 ; WX 511.11 ; N p ; B 12 -194 512 442 ; +C 113 ; WX 460 ; N q ; B 107 -194 499 442 ; +C 114 ; WX 421.67 ; N r ; B 81 -11 488 442 ; +C 115 ; WX 408.89 ; N s ; B 76 -11 419 442 ; +C 116 ; WX 332.22 ; N t ; B 90 -11 373 626 ; +C 117 ; WX 536.67 ; N u ; B 81 -11 564 442 ; +C 118 ; WX 460 ; N v ; B 81 -11 492 443 ; +C 119 ; WX 664.44 ; N w ; B 81 -11 696 443 ; +C 120 ; WX 463.89 ; N x ; B 55 -11 517 442 ; +C 121 ; WX 485.56 ; N y ; B 81 -205 517 442 ; +C 122 ; WX 408.89 ; N z ; B 60 -11 465 442 ; +C 123 ; WX 511.11 ; N endash ; B 92 253 552 279 ; L hyphen emdash ; +C 124 ; WX 1022.22 ; N emdash ; B 118 253 1037 279 ; +C 125 ; WX 511.11 ; N hungarumlaut ; B 267 505 576 697 ; +C 126 ; WX 511.11 ; N tilde ; B 248 565 572 668 ; +C 127 ; WX 511.11 ; N dieresis ; B 268 565 552 669 ; +C -1 ; WX 357.78 ; N space ; B 357 0 358 0 ; +EndCharMetrics +StartKernData +StartKernPairs 180 +KPX A C -25.56 +KPX A G -25.56 +KPX A O -25.56 +KPX A Q -25.56 +KPX A T -76.67 +KPX A U -25.56 +KPX A V -102.22 +KPX A W -102.22 +KPX A Y -76.67 +KPX A a -51.11 +KPX A b -25.56 +KPX A c -51.11 +KPX A d -51.11 +KPX A e -51.11 +KPX A g -51.11 +KPX A h -25.56 +KPX A i -25.56 +KPX A k -25.56 +KPX A l -25.56 +KPX A m -25.56 +KPX A n -25.56 +KPX A o -51.11 +KPX A q -51.11 +KPX A r -25.56 +KPX A t -25.56 +KPX A u -25.56 +KPX A v -25.56 +KPX A w -25.56 +KPX D A -25.56 +KPX D V -25.56 +KPX D W -25.56 +KPX D X -25.56 +KPX D Y -25.56 +KPX F A -102.22 +KPX F C -25.56 +KPX F G -25.56 +KPX F O -25.56 +KPX F Q -25.56 +KPX F a -76.67 +KPX F e -76.67 +KPX F o -76.67 +KPX F r -76.67 +KPX F u -76.67 +KPX K C -25.56 +KPX K G -25.56 +KPX K O -25.56 +KPX K Q -25.56 +KPX L T -76.67 +KPX L V -102.22 +KPX L W -102.22 +KPX L Y -76.67 +KPX L a -51.11 +KPX L c -51.11 +KPX L d -51.11 +KPX L e -51.11 +KPX L g -51.11 +KPX L o -51.11 +KPX L q -51.11 +KPX O A -25.56 +KPX O V -25.56 +KPX O W -25.56 +KPX O X -25.56 +KPX O Y -25.56 +KPX P A -76.67 +KPX R C -25.56 +KPX R G -25.56 +KPX R O -25.56 +KPX R Q -25.56 +KPX R T -76.67 +KPX R U -25.56 +KPX R V -102.22 +KPX R W -102.22 +KPX R Y -76.67 +KPX R a -51.11 +KPX R b -25.56 +KPX R c -51.11 +KPX R d -51.11 +KPX R e -51.11 +KPX R g -51.11 +KPX R h -25.56 +KPX R i -25.56 +KPX R k -25.56 +KPX R l -25.56 +KPX R m -25.56 +KPX R n -25.56 +KPX R o -51.11 +KPX R q -51.11 +KPX R r -25.56 +KPX R t -25.56 +KPX R u -25.56 +KPX R v -25.56 +KPX R w -25.56 +KPX T A -76.67 +KPX T a -76.67 +KPX T e -76.67 +KPX T o -76.67 +KPX T r -76.67 +KPX T u -76.67 +KPX T y -76.67 +KPX V A -102.22 +KPX V C -25.56 +KPX V G -25.56 +KPX V O -25.56 +KPX V Q -25.56 +KPX V a -76.67 +KPX V e -76.67 +KPX V o -76.67 +KPX V r -76.67 +KPX V u -76.67 +KPX W A -76.67 +KPX X C -25.56 +KPX X G -25.56 +KPX X O -25.56 +KPX X Q -25.56 +KPX Y A -76.67 +KPX Y a -76.67 +KPX Y e -76.67 +KPX Y o -76.67 +KPX Y r -76.67 +KPX Y u -76.67 +KPX b a -51.11 +KPX b c -51.11 +KPX b d -51.11 +KPX b e -51.11 +KPX b g -51.11 +KPX b o -51.11 +KPX b q -51.11 +KPX c a -51.11 +KPX c c -51.11 +KPX c d -51.11 +KPX c e -51.11 +KPX c g -51.11 +KPX c o -51.11 +KPX c q -51.11 +KPX d l 51.11 +KPX e a -51.11 +KPX e c -51.11 +KPX e d -51.11 +KPX e e -51.11 +KPX e g -51.11 +KPX e o -51.11 +KPX e q -51.11 +KPX f bracketright 104.31 +KPX f exclam 104.31 +KPX f parenright 104.31 +KPX f question 104.31 +KPX f quoteright 104.31 +KPX ff bracketright 104.31 +KPX ff exclam 104.31 +KPX ff parenright 104.31 +KPX ff question 104.31 +KPX ff quoteright 104.31 +KPX l l 51.11 +KPX n quoteright -102.22 +KPX o a -51.11 +KPX o c -51.11 +KPX o d -51.11 +KPX o e -51.11 +KPX o g -51.11 +KPX o o -51.11 +KPX o q -51.11 +KPX p a -51.11 +KPX p c -51.11 +KPX p d -51.11 +KPX p e -51.11 +KPX p g -51.11 +KPX p o -51.11 +KPX p q -51.11 +KPX polishlcross L -320.55 +KPX polishlcross l -255.55 +KPX quoteright exclam 102.22 +KPX quoteright question 102.22 +KPX r a -51.11 +KPX r c -51.11 +KPX r d -51.11 +KPX r e -51.11 +KPX r g -51.11 +KPX r o -51.11 +KPX r q -51.11 +KPX w l 51.11 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/lib/matplotlib/mpl-data/fonts/ttf/cmti10.ttf b/lib/matplotlib/mpl-data/fonts/ttf/cmti10.ttf new file mode 100644 index 000000000000..8311a98ac9e3 Binary files /dev/null and b/lib/matplotlib/mpl-data/fonts/ttf/cmti10.ttf differ diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index e934109ee492..67fd6c0d18be 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -292,6 +292,11 @@ ## for more information on text properties #text.color: black +## The language of the text in a format accepted by libraqm, namely `a BCP47 language +## code `_. If None, then no +## particular language will be implied, and default font settings will be used. +#text.language: None + ## FreeType hinting flag ("foo" corresponds to FT_LOAD_FOO); may be one of the ## following (Proprietary Matplotlib-specific synonyms are given in parentheses, ## but their use is discouraged): @@ -301,15 +306,14 @@ ## ("native" is a synonym.) ## - force_autohint: Use FreeType's auto-hinter. ("auto" is a synonym.) ## - no_hinting: Disable hinting. ("none" is a synonym.) -#text.hinting: force_autohint +#text.hinting: default -#text.hinting_factor: 8 # Specifies the amount of softness for hinting in the +#text.hinting_factor: 1 # Specifies the amount of softness for hinting in the # horizontal direction. A value of 1 will hint to full # pixels. A value of 2 will hint to half pixels etc. -#text.kerning_factor: 0 # Specifies the scaling factor for kerning values. This - # is provided solely to allow old test images to remain - # unchanged. Set to 6 to obtain previous behavior. - # Values other than 0 or 6 have no defined meaning. +#text.kerning_factor: None # Specifies the scaling factor for kerning values. Values + # other than 0, 6, or None have no defined meaning. + # This setting is deprecated. #text.antialiased: True # If True (default), the text will be antialiased. # This only affects raster outputs. #text.parse_math: True # Use mathtext if there is an even number of unescaped @@ -327,6 +331,16 @@ # zapf chancery, charter, serif, sans-serif, helvetica, # avant garde, courier, monospace, computer modern roman, # computer modern sans serif, computer modern typewriter + +## The TeX engine/format to use. The following values are supported: +## - "latex": The classic TeX engine (the current default). All backends render +## TeX's output by parsing the DVI output into glyphs and boxes and emitting +## those one by one. +## - "latex+dvipng": The same as "latex", with the exception that Agg-based +## backends rely on dvipng to rasterize TeX's output. This value was the +## default up to Matplotlib 3.10. +#text.latex.engine: latex + #text.latex.preamble: # IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES # AND IS THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP # IF THIS FEATURE DOES NOT DO WHAT YOU EXPECT IT TO. @@ -361,10 +375,10 @@ # 'stixsans'] when a symbol cannot be found in one of the # custom math fonts. Select 'None' to not perform fallback # and replace the missing character by a dummy symbol. -#mathtext.default: it # The default font to use for math. - # Can be any of the LaTeX font names, including - # the special name "regular" for the same font - # used in regular text. +#mathtext.default: normal # The default font to use for math. + # Can be any of the LaTeX font names (normal, it, bf, + # etc.), including the special name "regular" for the + # same font used in regular text. ## *************************************************************************** diff --git a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle index abd972925871..3dc92f832b20 100644 --- a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle @@ -1,8 +1,9 @@ # This patch should go on top of the "classic" style and exists solely to avoid # changing baseline images. -text.kerning_factor : 6 - ytick.alignment: center_baseline hatch.color: edge + +text.hinting: default +text.hinting_factor: 1 diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index cd636d65c7c8..302a25ca29a9 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -162,10 +162,10 @@ mathtext.fallback: cm # Select fallback font from ['cm' (Computer Modern), 'sti # custom math fonts. Select 'None' to not perform fallback # and replace the missing character by a dummy. -mathtext.default : it # The default font to use for math. - # Can be any of the LaTeX font names, including - # the special name "regular" for the same font - # used in regular text. +mathtext.default: normal # The default font to use for math. + # Can be any of the LaTeX font names (normal, it, bf, + # etc.), including the special name "regular" for the + # same font used in regular text. ### AXES # default face and edge color, default tick sizes, diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 9b9c7a69f35f..ca19a26f2b17 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -799,23 +799,24 @@ def get_bbox(self, renderer): ismath="TeX" if self._text.get_usetex() else False, dpi=self.get_figure(root=True).dpi) - bbox, info, yd = self._text._get_layout(renderer) + bbox, info, _ = self._text._get_layout(renderer) + _last_line, (_last_width, _last_ascent, last_descent), _last_xy = info[-1] w, h = bbox.size self._baseline_transform.clear() if len(info) > 1 and self._multilinebaseline: yd_new = 0.5 * h - 0.5 * (h_ - d_) - self._baseline_transform.translate(0, yd - yd_new) - yd = yd_new + self._baseline_transform.translate(0, last_descent - yd_new) + last_descent = yd_new else: # single line - h_d = max(h_ - d_, h - yd) - h = h_d + yd + h_d = max(h_ - d_, h - last_descent) + h = h_d + last_descent ha = self._text.get_horizontalalignment() x0 = {"left": 0, "center": -w / 2, "right": -w}[ha] - return Bbox.from_bounds(x0, -yd, w, h) + return Bbox.from_bounds(x0, -last_descent, w, h) def draw(self, renderer): # docstring inherited diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e84a8c70bc48..eefbae5e68f5 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1111,13 +1111,15 @@ def _convert_validator_spec(key, conv): # text props "text.color": validate_color, "text.usetex": validate_bool, + "text.latex.engine": ["latex", "latex+dvipng"], "text.latex.preamble": validate_string, "text.hinting": ["default", "no_autohint", "force_autohint", "no_hinting", "auto", "native", "either", "none"], "text.hinting_factor": validate_int, - "text.kerning_factor": validate_int, + "text.kerning_factor": validate_int_or_None, "text.antialiased": validate_bool, "text.parse_math": validate_bool, + "text.language": validate_string_or_None, "mathtext.cal": validate_font_properties, "mathtext.rm": validate_font_properties, @@ -1129,7 +1131,7 @@ def _convert_validator_spec(key, conv): "mathtext.fontset": ["dejavusans", "dejavuserif", "cm", "stix", "stixsans", "custom"], "mathtext.default": ["rm", "cal", "bfit", "it", "tt", "sf", "bf", "default", - "bb", "frak", "scr", "regular"], + "bb", "frak", "scr", "regular", "normal"], "mathtext.fallback": _validate_mathtext_fallback, "image.aspect": validate_aspect, # equal, auto, a number @@ -1843,8 +1845,19 @@ class _Subsection: default="black", validator=validate_color ), - _Param("text.hinting", - default="force_autohint", + _Param( + "text.language", + default=None, + validator=validate_string_or_None, + description="The language of the text in a format accepted by libraqm, namely " + "`a BCP47 language code " + "`_. If " + "None, then no particular language will be implied, and default " + "font settings will be used." + ), + _Param( + "text.hinting", + default="default", validator=[ "default", "no_autohint", "force_autohint", "no_hinting", "auto", "native", "either", "none", @@ -1862,7 +1875,7 @@ class _Subsection: ), _Param( "text.hinting_factor", - default=8, + default=1, validator=validate_int, description="Specifies the amount of softness for hinting in the horizontal " "direction. A value of 1 will hint to full pixels. A value of 2 " @@ -1870,12 +1883,12 @@ class _Subsection: ), _Param( "text.kerning_factor", - default=0, - validator=validate_int, - description="Specifies the scaling factor for kerning values. This is " - "provided solely to allow old test images to remain unchanged. " - "Set to 6 to obtain previous behavior. Values other than 0 or 6 " - "have no defined meaning." + default=None, + validator=validate_int_or_None, + description="[DEPRECATED] Specifies the scaling factor for kerning values. " + "This is provided solely to allow old test images to remain " + "unchanged. Set to 6 to obtain previous behavior. Values other " + "than 0 or 6 have no defined meaning." ), _Param( "text.antialiased", @@ -1903,6 +1916,20 @@ class _Subsection: "monospace, computer modern roman, computer modern sans serif, " "computer modern typewriter" ), + _Param( + "text.latex.engine", + default="latex", + validator=["latex", "latex+dvipng"], + description=( + "The TeX engine/format to use. The following values are supported:\n" + "- 'latex': The classic TeX engine (the current default). All backends " + "render TeX's output by parsing the DVI output into glyphs and boxes and " + "emitting those one by one.\n" + "- 'latex+dvipng': The same as 'latex', with the exception that Agg-based " + "backends rely on dvipng to rasterize TeX's output. This value was the " + "default up to Matplotlib 3.10." + ) + ), _Param( "text.latex.preamble", default="", @@ -1943,9 +1970,9 @@ class _Subsection: "math fonts. Select 'None' to not perform fallback and replace the " "missing character by a dummy symbol." ), - _Param("mathtext.default", "it", + _Param("mathtext.default", "normal", ["rm", "cal", "bfit", "it", "tt", "sf", "bf", "default", "bb", "frak", "scr", - "regular", ], + "regular", "normal"], description='The default font to use for math. Can be any of the LaTeX font ' 'names, including the special name "regular" for the same font ' 'used in regular text.', diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index eae1bfefa211..9fcdb6aeee03 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -19,8 +19,15 @@ def set_font_settings_for_testing(): mpl.rcParams['font.family'] = 'DejaVu Sans' - mpl.rcParams['text.hinting'] = 'none' - mpl.rcParams['text.hinting_factor'] = 8 + # We've changed the default for ourselves here, but for backwards-compatibility, use + # the old setting if not called in our own tests (which would set + # `_called_from_pytest` from our `conftest.py`). + if getattr(mpl, '_called_from_pytest', False): + mpl.rcParams['text.hinting'] = 'default' + mpl.rcParams['text.hinting_factor'] = 1 + else: + mpl.rcParams['text.hinting'] = 'none' + mpl.rcParams['text.hinting_factor'] = 8 def set_reproducibility_for_testing(): @@ -295,11 +302,13 @@ def _gen_multi_font_text(): latin1_supplement = [chr(x) for x in range(start, 0xFF+1)] latin_extended_A = [chr(x) for x in range(0x100, 0x17F+1)] latin_extended_B = [chr(x) for x in range(0x180, 0x24F+1)] + non_basic_multilingual_plane = [chr(x) for x in range(0x1F600, 0x1F610)] count = itertools.count(start - 0xA0) non_basic_characters = '\n'.join( ''.join(line) for _, line in itertools.groupby( # Replace with itertools.batched for Py3.12+. - [*latin1_supplement, *latin_extended_A, *latin_extended_B], + [*latin1_supplement, *latin_extended_A, *latin_extended_B, + *non_basic_multilingual_plane], key=lambda x: next(count) // 32) # 32 characters per line. ) test_str = f"""There are basic characters diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 6f87d9826cc3..c60a38254aad 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -149,6 +149,15 @@ def text_placeholders(monkeypatch): """ from matplotlib.patches import Rectangle + def patched_get_sfnt_table(font, name): + """ + Replace ``FT2Font.get_sfnt_table`` with empty results. + + This forces ``Text._get_layout`` to fall back to + ``get_text_width_height_descent``, which produces results from the patch below. + """ + return None + def patched_get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): """ Replace ``_get_text_metrics_with_cache`` with fixed results. @@ -183,6 +192,8 @@ def patched_text_draw(self, renderer): facecolor=self.get_color(), edgecolor='none') rect.draw(renderer) + monkeypatch.setattr('matplotlib.ft2font.FT2Font.get_sfnt_table', + patched_get_sfnt_table) monkeypatch.setattr('matplotlib.text._get_text_metrics_with_cache', patched_get_text_metrics_with_cache) monkeypatch.setattr('matplotlib.text.Text.draw', patched_text_draw) diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf index 5cdc2e34e25d..b7dbb9adec70 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/pdf_use14corefonts.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf new file mode 100644 index 000000000000..a3fad0172364 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf new file mode 100644 index 000000000000..d43f233ef4e6 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf new file mode 100644 index 000000000000..5d695e734577 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type3.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type3.eps new file mode 100644 index 000000000000..e066abad29f5 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type3.eps @@ -0,0 +1,1483 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%LanguageLevel: 3 +%%Title: ttc_type3.eps +%%Creator: Matplotlib v3.11.0.dev1121+gba80e42970.d20250725, https://matplotlib.org/ +%%CreationDate: Fri Jul 25 06:03:10 2025 +%%Orientation: portrait +%%BoundingBox: 0 0 504 72 +%%HiResBoundingBox: 0.000000 0.000000 504.000000 72.000000 +%%EndComments +%%BeginProlog +/mpldict 10 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/sc { setcachedevice } _d +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /WenQuanYiZenHei def +/PaintType 0 def +/FontMatrix [0.0009765625 0 0 0.0009765625 0 0] def +/FontBBox [-129 -304 1076 986] def +/FontType 3 def +/Encoding [/space /colon /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /a /e /i /n /u] def +/CharStrings 34 dict dup begin +/.notdef 0 def +/space{307 0 0 0 0 0 sc +ce} _d +/colon{307 0 101 0 215 520 sc +215 404 m +101 404 l +101 520 l +215 520 l +215 404 l + +215 0 m +101 0 l +101 116 l +215 116 l +215 0 l + +ce} _d +/A{573 0 11 0 568 702 sc +568 0 m +478 0 l +408 205 l +147 205 l +85 0 l +11 0 l +239 702 l +340 702 l +568 0 l + +388 271 m +281 608 l +172 271 l +388 271 l + +ce} _d +/B{634 0 83 0 588 702 sc +588 194 m +588 130 565 80 520 45 c +484 16 434 1 370 0 c +347 0 l +83 0 l +83 702 l +348 702 l +405 702 448 694 477 679 c +482 676 487 673 493 670 c +541 637 565 591 566 531 c +566 470 542 424 494 394 c +462 380 l +456 377 449 375 442 374 c +442 372 l +501 359 543 327 568 276 c +581 251 588 224 588 194 c + +479 520 m +479 585 443 621 370 630 c +361 631 351 631 341 631 c +171 631 l +171 401 l +317 401 l +425 401 479 441 479 520 c + +500 198 m +500 241 485 276 454 303 c +453 304 451 306 449 307 c +426 325 389 334 338 334 c +171 334 l +171 75 l +343 75 l +432 75 483 105 497 166 c +499 175 500 186 500 198 c + +ce} _d +/C{634 0 43 -10 589 713 sc +589 219 m +558 90 490 16 386 -5 c +367 -8 348 -10 329 -10 c +224 -10 146 38 94 135 c +60 199 43 273 43 358 c +43 471 72 560 131 626 c +183 684 251 713 336 713 c +435 713 508 669 555 582 c +570 554 582 523 589 488 c +506 473 l +479 573 430 628 359 637 c +350 638 342 639 333 639 c +248 639 190 590 159 492 c +146 449 139 402 139 351 c +139 261 158 189 197 134 c +230 87 274 63 329 63 c +418 63 477 117 506 225 c +507 229 508 233 509 237 c +589 219 l + +ce} _d +/D{675 0 87 0 636 702 sc +636 353 m +636 247 607 160 550 93 c +496 31 425 0 338 0 c +87 0 l +87 702 l +309 702 l +394 702 462 682 512 642 c +525 631 538 618 551 603 c +608 537 636 454 636 353 c + +547 353 m +547 436 525 504 482 557 c +449 599 403 623 344 628 c +304 629 l +175 629 l +175 75 l +304 75 l +382 75 440 97 478 140 c +484 147 490 155 496 164 c +530 216 547 279 547 353 c + +ce} _d +/E{573 0 87 0 542 702 sc +542 0 m +87 0 l +87 702 l +531 702 l +531 627 l +175 627 l +175 403 l +458 403 l +458 333 l +175 333 l +175 76 l +542 76 l +542 0 l + +ce} _d +/F{491 0 84 0 514 702 sc +514 627 m +172 627 l +172 403 l +456 403 l +456 333 l +172 333 l +172 0 l +84 0 l +84 702 l +514 702 l +514 627 l + +ce} _d +/G{675 0 49 -10 614 713 sc +614 -5 m +560 -5 l +537 82 l +497 24 436 -7 355 -10 c +350 -10 346 -10 342 -10 c +237 -10 157 35 104 125 c +67 187 49 260 49 344 c +49 452 77 541 133 610 c +188 679 262 713 353 713 c +457 713 532 670 579 585 c +591 563 600 538 607 511 c +524 490 l +512 553 480 597 428 622 c +403 633 376 639 347 639 c +256 639 195 589 164 488 c +151 447 144 400 144 348 c +144 251 167 176 212 123 c +247 82 292 61 348 61 c +418 61 469 88 500 141 c +516 170 524 203 524 242 c +524 281 l +349 281 l +349 354 l +614 354 l +614 -5 l + +ce} _d +/H{675 0 83 0 585 702 sc +585 0 m +497 0 l +497 334 l +171 334 l +171 0 l +83 0 l +83 702 l +171 702 l +171 411 l +497 411 l +497 702 l +585 702 l +585 0 l + +ce} _d +/I{266 0 88 0 176 702 sc +176 0 m +88 0 l +88 702 l +176 702 l +176 0 l + +ce} _d +/J{409 0 17 -10 331 702 sc +331 229 m +331 104 290 29 207 2 c +182 -6 153 -10 120 -10 c +86 -10 52 -5 17 4 c +17 76 l +50 69 81 66 108 66 c +171 66 211 83 227 118 c +238 139 243 176 243 229 c +243 702 l +331 702 l +331 229 l + +ce} _d +/K{634 0 88 0 643 702 sc +643 0 m +546 0 l +337 387 l +176 187 l +176 0 l +88 0 l +88 702 l +176 702 l +176 295 l +493 702 l +588 702 l +398 457 l +643 0 l + +ce} _d +/L{512 0 86 0 501 702 sc +501 0 m +86 0 l +86 702 l +174 702 l +174 78 l +501 78 l +501 0 l + +ce} _d +/M{839 0 82 0 761 702 sc +761 0 m +673 0 l +673 613 l +669 613 l +446 0 l +387 0 l +160 613 l +156 613 l +156 0 l +82 0 l +82 702 l +213 702 l +425 140 l +632 702 l +761 702 l +761 0 l + +ce} _d +/N{675 0 84 0 601 702 sc +601 0 m +515 0 l +158 612 l +158 0 l +84 0 l +84 702 l +195 702 l +527 130 l +527 702 l +601 702 l +601 0 l + +ce} _d +/O{675 0 45 -10 623 713 sc +623 359 m +623 258 598 173 548 102 c +495 27 424 -10 335 -10 c +230 -10 151 36 98 128 c +63 189 45 262 45 346 c +45 453 71 540 124 609 c +176 678 246 713 334 713 c +435 713 513 669 566 580 c +604 517 623 443 623 359 c + +530 354 m +530 449 509 522 468 575 c +435 618 392 640 337 640 c +250 640 191 591 159 492 c +144 448 137 399 137 345 c +137 258 157 187 197 133 c +232 85 278 61 335 61 c +417 61 474 108 507 203 c +522 248 530 299 530 354 c + +ce} _d +/P{573 0 66 0 542 702 sc +542 492 m +542 436 523 388 485 348 c +480 343 l +441 305 385 286 311 286 c +154 286 l +154 0 l +66 0 l +66 702 l +301 702 l +376 702 433 686 470 655 c +510 622 534 575 541 515 c +542 507 542 499 542 492 c + +453 492 m +453 557 425 600 370 620 c +351 626 331 629 308 629 c +154 629 l +154 358 l +302 358 l +375 358 422 384 442 435 c +449 452 453 471 453 492 c + +ce} _d +/Q{675 0 42 -167 626 713 sc +626 351 m +626 244 599 158 546 91 c +509 46 462 16 407 1 c +407 -14 l +407 -55 425 -80 461 -88 c +468 -90 476 -91 485 -91 c +500 -91 532 -89 580 -84 c +580 -154 l +553 -163 528 -167 504 -167 c +412 -167 357 -130 340 -55 c +337 -41 335 -26 335 -10 c +224 -7 142 41 91 135 c +58 195 42 265 42 346 c +42 456 69 545 124 613 c +177 680 247 713 334 713 c +441 713 520 666 572 572 c +608 509 626 436 626 351 c + +530 349 m +530 482 496 569 428 610 c +401 627 370 635 333 635 c +234 635 172 576 147 459 c +140 424 136 386 136 344 c +136 239 160 161 207 112 c +238 79 278 62 327 62 c +425 62 488 116 516 225 c +525 263 530 304 530 349 c + +ce} _d +/R{634 0 83 0 588 702 sc +588 0 m +496 0 l +366 304 l +171 304 l +171 0 l +83 0 l +83 702 l +346 702 l +436 702 502 676 544 624 c +563 599 575 568 580 532 c +581 524 581 515 581 506 c +581 445 559 396 515 359 c +496 344 474 333 450 326 c +588 0 l + +493 507 m +493 577 454 616 377 625 c +367 626 357 627 346 627 c +171 627 l +171 376 l +336 376 l +422 376 472 406 487 467 c +491 479 493 492 493 507 c + +ce} _d +/S{634 0 43 -10 590 713 sc +590 201 m +590 114 550 53 469 17 c +428 -1 381 -10 328 -10 c +184 -10 89 56 43 189 c +122 207 l +143 134 191 89 266 72 c +286 67 307 65 330 65 c +398 65 447 83 476 120 c +491 139 499 162 499 189 c +499 237 469 273 408 296 c +343 314 l +264 334 221 345 214 348 c +179 359 153 373 135 389 c +97 423 78 466 78 519 c +78 599 115 655 189 688 c +227 705 269 713 316 713 c +413 713 485 678 534 608 c +554 571 l +559 562 563 552 566 541 c +486 519 l +477 565 448 600 398 624 c +371 637 343 643 314 643 c +265 643 226 629 195 601 c +174 582 164 558 164 531 c +164 485 193 451 250 430 c +264 425 308 414 383 397 c +450 382 497 363 524 340 c +568 305 590 258 590 201 c + +ce} _d +/T{512 0 11 0 499 702 sc +499 626 m +299 626 l +299 0 l +211 0 l +211 626 l +11 626 l +11 702 l +499 702 l +499 626 l + +ce} _d +/U{655 0 83 -10 567 702 sc +567 258 m +567 184 556 128 534 91 c +528 83 522 75 516 68 c +473 16 410 -10 327 -10 c +210 -10 135 32 103 117 c +90 154 83 201 83 258 c +83 702 l +171 702 l +171 258 l +171 179 187 126 218 99 c +243 78 282 68 334 68 c +417 68 468 103 485 174 c +491 197 494 225 494 258 c +494 702 l +567 702 l +567 258 l + +ce} _d +/V{573 0 10 0 569 702 sc +569 702 m +332 0 l +248 0 l +10 702 l +102 702 l +298 123 l +493 702 l +569 702 l + +ce} _d +/W{839 0 11 0 831 702 sc +831 702 m +663 0 l +574 0 l +422 545 l +279 0 l +189 0 l +11 702 l +101 702 l +242 133 l +244 133 l +392 702 l +463 702 l +623 133 l +625 133 l +755 702 l +831 702 l + +ce} _d +/X{552 0 9 0 552 702 sc +552 0 m +453 0 l +275 299 l +91 0 l +9 0 l +234 364 l +34 702 l +132 702 l +287 442 l +443 702 l +524 702 l +328 381 l +552 0 l + +ce} _d +/Y{573 0 11 0 568 702 sc +568 702 m +334 296 l +334 0 l +246 0 l +246 296 l +11 702 l +114 702 l +300 379 l +488 702 l +568 702 l + +ce} _d +/Z{552 0 17 0 513 702 sc +513 0 m +17 0 l +17 76 l +399 630 l +35 630 l +35 702 l +502 702 l +502 644 l +116 76 l +513 76 l +513 0 l + +ce} _d +/a{552 0 56 -10 509 534 sc +509 0 m +429 0 l +420 97 l +385 26 324 -10 238 -10 c +171 -10 120 11 87 53 c +66 79 56 110 56 147 c +56 198 78 240 122 273 c +131 281 142 287 153 292 c +198 313 272 324 375 323 c +420 323 l +420 345 l +420 409 397 448 352 463 c +316 470 l +307 471 298 471 288 471 c +205 471 158 440 145 379 c +71 391 l +80 458 124 502 205 522 c +233 530 263 534 295 534 c +384 534 442 510 469 463 c +488 431 498 380 498 311 c +498 110 l +498 63 502 26 509 0 c + +420 228 m +420 262 l +353 262 l +212 262 142 222 142 143 c +142 100 165 71 211 58 c +224 54 239 52 255 52 c +312 52 356 76 388 123 c +409 154 420 189 420 228 c + +ce} _d +/e{552 0 38 -10 506 534 sc +506 254 m +128 254 l +127 187 143 136 174 101 c +200 72 236 57 283 57 c +341 57 384 82 411 133 c +415 140 419 149 422 158 c +498 142 l +479 82 439 38 378 11 c +346 -3 312 -10 276 -10 c +187 -10 121 27 78 100 c +51 145 38 199 38 260 c +38 346 64 415 116 468 c +159 512 213 534 279 534 c +369 534 434 495 473 417 c +495 374 506 323 506 266 c +506 254 l + +418 313 m +419 370 401 413 366 442 c +342 462 313 472 279 472 c +226 472 186 448 158 400 c +142 374 133 345 132 313 c +418 313 l + +ce} _d +/i{245 0 79 0 167 702 sc +167 612 m +79 612 l +79 702 l +167 702 l +167 612 l + +163 0 m +83 0 l +83 520 l +163 520 l +163 0 l + +ce} _d +/n{552 0 71 0 482 534 sc +482 0 m +402 0 l +402 322 l +402 366 398 396 391 412 c +390 414 389 416 388 418 c +373 444 348 459 313 464 c +308 465 304 465 299 465 c +234 465 190 432 166 366 c +156 337 151 306 151 272 c +151 0 l +71 0 l +71 520 l +146 520 l +146 424 l +148 424 l +173 477 211 511 263 526 c +278 531 294 534 310 534 c +375 534 423 508 455 457 c +458 453 l +474 425 482 377 482 309 c +482 0 l + +ce} _d +/u{552 0 71 -10 479 520 sc +479 0 m +407 0 l +407 103 l +404 103 l +386 58 355 25 310 4 c +289 -5 268 -10 245 -10 c +144 -10 87 45 74 156 c +72 171 71 187 71 204 c +71 520 l +151 520 l +151 204 l +151 113 181 65 242 59 c +242 59 245 59 252 59 c +311 59 354 89 380 148 c +393 177 399 209 399 244 c +399 520 l +479 520 l +479 0 l + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /WenQuanYiZenHeiMono def +/PaintType 0 def +/FontMatrix [0.0009765625 0 0 0.0009765625 0 0] def +/FontBBox [-129 -304 1076 986] def +/FontType 3 def +/Encoding [/space /colon /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /a /e /i /n /o /u] def +/CharStrings 35 dict dup begin +/.notdef 0 def +/space{512 0 0 0 0 0 sc +ce} _d +/colon{512 0 195 18 317 571 sc +195 418 m +195 571 l +317 571 l +317 418 l +195 418 l + +195 18 m +195 172 l +317 172 l +317 18 l +195 18 l + +ce} _d +/A{512 0 20 18 492 766 sc +255 694 m +253 694 l +168 305 l +340 305 l +255 694 l + +356 233 m +152 233 l +104 18 l +20 18 l +205 766 l +307 766 l +492 18 l +403 18 l +356 233 l + +ce} _d +/B{512 0 77 8 466 776 sc +161 459 m +207 459 l +262 459 301 469 326 489 c +351 509 364 540 364 582 c +364 621 352 651 327 672 c +302 694 267 705 222 705 c +197 705 177 702 161 696 c +161 459 l + +161 387 m +161 88 l +186 83 217 80 253 80 c +339 80 382 135 382 244 c +382 339 327 387 217 387 c +161 387 l + +466 233 m +466 83 389 8 236 8 c +181 8 128 13 77 24 c +77 761 l +128 771 181 776 236 776 c +375 776 445 715 445 592 c +445 550 435 515 414 486 c +393 457 365 438 328 429 c +328 427 l +367 420 400 398 426 361 c +453 325 466 282 466 233 c + +ce} _d +/C{512 0 56 8 435 776 sc +56 392 m +56 527 77 624 118 685 c +159 746 221 776 302 776 c +347 776 390 766 430 745 c +430 669 l +389 691 348 702 307 702 c +194 702 138 599 138 392 c +138 280 152 200 181 153 c +210 106 252 82 307 82 c +350 82 392 95 435 121 c +435 39 l +395 18 351 8 302 8 c +219 8 157 37 116 96 c +76 155 56 253 56 392 c + +ce} _d +/D{512 0 67 8 476 776 sc +392 392 m +392 507 377 587 347 633 c +318 679 271 702 207 702 c +184 702 166 699 151 694 c +151 90 l +166 85 184 82 207 82 c +251 82 286 90 311 107 c +337 124 357 155 371 200 c +385 246 392 310 392 392 c + +476 392 m +476 251 454 151 411 94 c +368 37 300 8 207 8 c +159 8 112 13 67 24 c +67 761 l +112 771 159 776 207 776 c +300 776 368 747 411 688 c +454 630 476 531 476 392 c + +ce} _d +/E{512 0 82 18 430 766 sc +166 692 m +166 459 l +420 459 l +420 387 l +166 387 l +166 92 l +430 92 l +430 18 l +82 18 l +82 766 l +430 766 l +430 692 l +166 692 l + +ce} _d +/F{512 0 92 18 430 766 sc +176 387 m +176 18 l +92 18 l +92 766 l +430 766 l +430 692 l +176 692 l +176 459 l +420 459 l +420 387 l +176 387 l + +ce} _d +/G{512 0 41 8 461 776 sc +379 105 m +379 387 l +220 387 l +220 459 l +461 459 l +461 49 l +406 22 348 8 287 8 c +206 8 145 38 103 99 c +62 160 41 257 41 392 c +41 526 63 623 106 684 c +149 745 215 776 302 776 c +342 776 386 768 435 751 c +435 672 l +390 692 346 702 302 702 c +243 702 198 677 167 628 c +136 579 121 501 121 392 c +121 185 178 82 292 82 c +323 82 352 90 379 105 c + +ce} _d +/H{512 0 61 18 451 766 sc +143 766 m +143 461 l +365 461 l +365 766 l +451 766 l +451 18 l +365 18 l +365 387 l +143 387 l +143 18 l +61 18 l +61 766 l +143 766 l + +ce} _d +/I{512 0 92 18 420 766 sc +420 18 m +92 18 l +92 90 l +213 90 l +213 694 l +92 694 l +92 766 l +420 766 l +420 694 l +299 694 l +299 90 l +420 90 l +420 18 l + +ce} _d +/J{512 0 61 8 410 766 sc +410 766 m +410 213 l +410 138 395 85 365 54 c +336 23 286 8 215 8 c +159 8 108 18 61 39 c +61 128 l +81 117 107 106 138 96 c +170 87 196 82 215 82 c +251 82 278 92 296 113 c +314 134 323 168 323 215 c +323 694 l +154 694 l +154 766 l +410 766 l + +ce} _d +/K{512 0 77 18 476 766 sc +161 428 m +163 428 l +374 766 l +471 766 l +241 408 l +476 18 l +379 18 l +163 387 l +161 387 l +161 18 l +77 18 l +77 766 l +161 766 l +161 428 l + +ce} _d +/L{512 0 102 18 430 766 sc +186 766 m +186 92 l +430 92 l +430 18 l +102 18 l +102 766 l +186 766 l + +ce} _d +/M{512 0 41 18 471 766 sc +387 571 m +385 571 l +295 223 l +213 223 l +123 571 l +121 571 l +121 18 l +41 18 l +41 766 l +135 766 l +257 305 l +259 305 l +381 766 l +471 766 l +471 18 l +387 18 l +387 571 l + +ce} _d +/N{512 0 67 18 445 766 sc +155 582 m +153 582 l +153 18 l +67 18 l +67 766 l +153 766 l +361 203 l +364 203 l +364 766 l +445 766 l +445 18 l +364 18 l +155 582 l + +ce} _d +/O{512 0 41 8 471 776 sc +93 689 m +128 747 183 776 256 776 c +329 776 383 747 418 689 c +453 632 471 533 471 392 c +471 251 453 152 418 94 c +383 37 329 8 256 8 c +183 8 128 37 93 94 c +58 152 41 251 41 392 c +41 533 58 632 93 689 c + +183 108 m +202 91 226 82 256 82 c +286 82 310 91 328 108 c +347 125 361 157 372 203 c +383 250 389 313 389 392 c +389 471 383 534 372 580 c +361 627 347 659 328 676 c +310 693 286 702 256 702 c +226 702 202 693 183 676 c +165 659 150 627 139 580 c +128 534 123 471 123 392 c +123 313 128 250 139 203 c +150 157 165 125 183 108 c + +ce} _d +/P{512 0 77 18 466 776 sc +384 551 m +384 605 372 644 347 668 c +322 693 284 705 232 705 c +204 705 180 702 161 696 c +161 397 l +181 394 205 392 232 392 c +285 392 323 404 347 428 c +372 453 384 494 384 551 c + +466 551 m +466 469 448 410 412 374 c +376 339 320 321 243 321 c +219 321 192 323 161 326 c +161 18 l +77 18 l +77 761 l +130 771 185 776 241 776 c +318 776 375 758 411 722 c +448 687 466 630 466 551 c + +ce} _d +/Q{512 0 41 -135 492 776 sc +93 689 m +128 747 183 776 256 776 c +329 776 383 747 418 689 c +453 632 471 533 471 392 c +471 206 437 89 369 40 c +369 38 l +397 23 422 1 443 -30 c +465 -61 481 -96 492 -135 c +401 -135 l +387 -82 369 -44 346 -23 c +323 -2 293 8 256 8 c +183 8 128 37 93 94 c +58 152 41 251 41 392 c +41 533 58 632 93 689 c + +183 108 m +202 91 226 82 256 82 c +286 82 310 91 328 108 c +347 125 361 157 372 203 c +383 250 389 313 389 392 c +389 471 383 534 372 580 c +361 627 347 659 328 676 c +310 693 286 702 256 702 c +226 702 202 693 183 676 c +165 659 150 627 139 580 c +128 534 123 471 123 392 c +123 313 128 250 139 203 c +150 157 165 125 183 108 c + +ce} _d +/R{512 0 72 18 481 776 sc +379 571 m +379 660 328 705 227 705 c +199 705 175 702 156 696 c +156 418 l +217 418 l +276 418 318 429 342 452 c +367 475 379 514 379 571 c + +156 346 m +156 18 l +72 18 l +72 761 l +125 771 180 776 236 776 c +312 776 368 759 405 725 c +442 692 461 640 461 571 c +461 474 424 408 349 374 c +349 372 l +370 361 393 317 417 238 c +481 18 l +393 18 l +333 240 l +321 283 307 312 290 325 c +274 339 246 346 207 346 c +156 346 l + +ce} _d +/S{512 0 72 8 451 776 sc +266 702 m +234 702 208 692 187 671 c +166 650 156 624 156 592 c +156 558 163 530 178 507 c +193 485 217 466 251 451 c +327 417 379 382 408 345 c +437 309 451 262 451 203 c +451 138 433 90 398 57 c +363 24 312 8 246 8 c +184 8 128 27 77 65 c +77 162 l +132 109 190 82 251 82 c +328 82 367 122 367 203 c +367 240 358 271 340 296 c +322 321 292 342 251 361 c +187 390 141 422 113 459 c +86 496 72 540 72 592 c +72 647 89 691 124 725 c +159 759 204 776 261 776 c +297 776 327 773 350 768 c +373 763 400 752 430 735 c +430 643 l +377 682 323 702 266 702 c + +ce} _d +/T{512 0 56 18 456 766 sc +214 18 m +214 694 l +56 694 l +56 766 l +456 766 l +456 694 l +298 694 l +298 18 l +214 18 l + +ce} _d +/U{512 0 61 8 451 766 sc +402 58 m +369 25 321 8 256 8 c +191 8 143 25 110 58 c +77 91 61 143 61 213 c +61 766 l +147 766 l +147 233 l +147 178 156 139 174 116 c +193 93 221 82 258 82 c +295 82 323 93 341 116 c +360 139 369 178 369 233 c +369 766 l +451 766 l +451 213 l +451 143 435 91 402 58 c + +ce} _d +/V{512 0 31 18 481 766 sc +259 90 m +397 766 l +481 766 l +307 18 l +205 18 l +31 766 l +119 766 l +257 90 l +259 90 l + +ce} _d +/W{512 0 26 18 486 766 sc +157 141 m +159 141 l +214 664 l +306 664 l +361 141 l +364 141 l +410 766 l +486 766 l +425 18 l +313 18 l +257 551 l +255 551 l +199 18 l +87 18 l +26 766 l +111 766 l +157 141 l + +ce} _d +/X{512 0 51 18 461 766 sc +257 469 m +259 469 l +374 766 l +459 766 l +307 402 l +461 18 l +369 18 l +255 331 l +253 331 l +138 18 l +51 18 l +205 402 l +53 766 l +143 766 l +257 469 l + +ce} _d +/Y{512 0 31 18 481 766 sc +257 402 m +259 402 l +394 766 l +481 766 l +298 315 l +298 18 l +214 18 l +214 315 l +31 766 l +123 766 l +257 402 l + +ce} _d +/Z{512 0 77 18 435 766 sc +343 692 m +343 694 l +77 694 l +77 766 l +435 766 l +435 694 l +169 92 l +169 90 l +435 90 l +435 18 l +77 18 l +77 90 l +343 692 l + +ce} _d +/a{512 0 61 8 440 561 sc +261 561 m +330 561 377 547 402 520 c +427 493 440 442 440 367 c +440 18 l +367 18 l +365 95 l +362 95 l +330 37 279 8 210 8 c +165 8 129 22 102 50 c +75 79 61 118 61 167 c +61 229 82 277 124 310 c +166 344 229 361 312 361 c +361 361 l +361 387 l +361 426 353 453 338 469 c +323 485 298 493 261 493 c +238 493 210 489 175 480 c +140 472 111 463 87 452 c +87 525 l +111 536 140 544 174 551 c +208 558 237 561 261 561 c + +361 300 m +312 300 l +196 300 138 257 138 172 c +138 141 146 118 161 101 c +177 85 198 77 225 77 c +266 77 298 93 323 126 c +348 159 361 205 361 264 c +361 300 l + +ce} _d +/e{512 0 56 8 445 561 sc +141 258 m +144 195 157 149 180 121 c +203 93 237 79 282 79 c +323 79 369 91 420 116 c +420 34 l +369 17 321 8 276 8 c +129 8 56 100 56 285 c +56 381 73 451 107 495 c +142 539 193 561 261 561 c +323 561 369 540 399 497 c +430 455 445 386 445 290 c +445 283 444 272 443 258 c +141 258 l + +141 326 m +365 326 l +364 435 330 490 261 490 c +222 490 193 478 174 453 c +155 429 144 387 141 326 c + +ce} _d +/i{512 0 97 18 435 797 sc +324 551 m +324 88 l +435 88 l +435 18 l +97 18 l +97 88 l +240 88 l +240 481 l +128 481 l +128 551 l +324 551 l + +219 674 m +219 797 l +324 797 l +324 674 l +219 674 l + +ce} _d +/n{512 0 72 18 451 561 sc +451 356 m +451 18 l +372 18 l +372 338 l +372 398 365 438 351 458 c +338 478 313 488 276 488 c +242 488 213 469 189 431 c +165 393 153 341 153 276 c +153 18 l +72 18 l +72 551 l +147 551 l +150 474 l +152 474 l +165 500 184 521 211 537 c +238 553 266 561 297 561 c +351 561 390 545 414 514 c +439 483 451 431 451 356 c + +ce} _d +/o{512 0 51 8 461 561 sc +51 284 m +51 469 119 561 256 561 c +393 561 461 469 461 284 c +461 100 393 8 256 8 c +119 8 51 100 51 284 c + +164 125 m +184 94 215 79 256 79 c +297 79 328 94 347 125 c +367 156 377 209 377 284 c +377 359 367 412 347 443 c +328 474 297 490 256 490 c +215 490 184 474 164 443 c +145 412 135 359 135 284 c +135 209 145 156 164 125 c + +ce} _d +/u{512 0 67 8 435 551 sc +67 203 m +67 551 l +145 551 l +145 221 l +145 164 151 127 164 108 c +177 90 201 81 236 81 c +269 81 297 100 320 137 c +343 175 354 227 354 293 c +354 551 l +435 551 l +435 18 l +359 18 l +357 95 l +355 95 l +342 68 323 46 298 31 c +273 16 245 8 215 8 c +162 8 124 23 101 52 c +78 81 67 132 67 203 c + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +0 0 translate +0 0 504 72 rectclip +gsave +0 0 m +504 0 l +504 72 l +0 72 l +cl +1 setgray +fill +grestore +gsave +0 36 m +504 36 l +504 72 l +0 72 l +0 36 l +cl +grestore +0 setgray +/WenQuanYiZenHei 16.000 selectfont +gsave + +55.4375 49.7812 translate +0 rotate +0 0 m /W glyphshow +13.1094 0 m /e glyphshow +21.7344 0 m /n glyphshow +30.3594 0 m /Q glyphshow +40.9062 0 m /u glyphshow +49.5312 0 m /a glyphshow +58.1562 0 m /n glyphshow +66.7812 0 m /Y glyphshow +75.7344 0 m /i glyphshow +79.5625 0 m /space glyphshow +84.3594 0 m /Z glyphshow +92.9844 0 m /e glyphshow +101.609 0 m /n glyphshow +110.234 0 m /space glyphshow +115.031 0 m /H glyphshow +125.578 0 m /e glyphshow +134.203 0 m /i glyphshow +138.031 0 m /colon glyphshow +142.828 0 m /space glyphshow +147.625 0 m /A glyphshow +156.578 0 m /B glyphshow +166.484 0 m /C glyphshow +176.391 0 m /D glyphshow +186.938 0 m /E glyphshow +195.891 0 m /F glyphshow +203.562 0 m /G glyphshow +214.109 0 m /H glyphshow +224.656 0 m /I glyphshow +228.812 0 m /J glyphshow +235.203 0 m /K glyphshow +245.109 0 m /L glyphshow +253.109 0 m /M glyphshow +266.219 0 m /N glyphshow +276.766 0 m /O glyphshow +287.312 0 m /P glyphshow +296.266 0 m /Q glyphshow +306.812 0 m /R glyphshow +316.719 0 m /S glyphshow +326.625 0 m /T glyphshow +334.625 0 m /U glyphshow +344.859 0 m /V glyphshow +353.812 0 m /W glyphshow +366.922 0 m /X glyphshow +375.547 0 m /Y glyphshow +384.5 0 m /Z glyphshow +grestore +gsave +0 0 m +504 0 l +504 36 l +0 36 l +0 0 l +cl +grestore +/WenQuanYiZenHeiMono 16.000 selectfont +gsave + +52 13.6328 translate +0 rotate +0 0 m /W glyphshow +8 0 m /e glyphshow +16 0 m /n glyphshow +24 0 m /Q glyphshow +32 0 m /u glyphshow +40 0 m /a glyphshow +48 0 m /n glyphshow +56 0 m /Y glyphshow +64 0 m /i glyphshow +72 0 m /space glyphshow +80 0 m /Z glyphshow +88 0 m /e glyphshow +96 0 m /n glyphshow +104 0 m /space glyphshow +112 0 m /H glyphshow +120 0 m /e glyphshow +128 0 m /i glyphshow +136 0 m /space glyphshow +144 0 m /M glyphshow +152 0 m /o glyphshow +160 0 m /n glyphshow +168 0 m /o glyphshow +176 0 m /colon glyphshow +184 0 m /space glyphshow +192 0 m /A glyphshow +200 0 m /B glyphshow +208 0 m /C glyphshow +216 0 m /D glyphshow +224 0 m /E glyphshow +232 0 m /F glyphshow +240 0 m /G glyphshow +248 0 m /H glyphshow +256 0 m /I glyphshow +264 0 m /J glyphshow +272 0 m /K glyphshow +280 0 m /L glyphshow +288 0 m /M glyphshow +296 0 m /N glyphshow +304 0 m /O glyphshow +312 0 m /P glyphshow +320 0 m /Q glyphshow +328 0 m /R glyphshow +336 0 m /S glyphshow +344 0 m /T glyphshow +352 0 m /U glyphshow +360 0 m /V glyphshow +368 0 m /W glyphshow +376 0 m /X glyphshow +384 0 m /Y glyphshow +392 0 m /Z glyphshow +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type42.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type42.eps new file mode 100644 index 000000000000..3df370bd885e --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type42.eps @@ -0,0 +1,1483 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%LanguageLevel: 3 +%%Title: ttc_type42.eps +%%Creator: Matplotlib v3.11.0.dev1121+gba80e42970.d20250725, https://matplotlib.org/ +%%CreationDate: Fri Jul 25 06:03:10 2025 +%%Orientation: portrait +%%BoundingBox: 0 0 504 72 +%%HiResBoundingBox: 0.000000 0.000000 504.000000 72.000000 +%%EndComments +%%BeginProlog +/mpldict 10 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/sc { setcachedevice } _d +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /WenQuanYiZenHei def +/PaintType 0 def +/FontMatrix [0.0009765625 0 0 0.0009765625 0 0] def +/FontBBox [-129 -304 1076 986] def +/FontType 3 def +/Encoding [/space /colon /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /a /e /i /n /u] def +/CharStrings 34 dict dup begin +/.notdef 0 def +/space{307 0 0 0 0 0 sc +ce} _d +/colon{307 0 101 0 215 520 sc +215 404 m +101 404 l +101 520 l +215 520 l +215 404 l + +215 0 m +101 0 l +101 116 l +215 116 l +215 0 l + +ce} _d +/A{573 0 11 0 568 702 sc +568 0 m +478 0 l +408 205 l +147 205 l +85 0 l +11 0 l +239 702 l +340 702 l +568 0 l + +388 271 m +281 608 l +172 271 l +388 271 l + +ce} _d +/B{634 0 83 0 588 702 sc +588 194 m +588 130 565 80 520 45 c +484 16 434 1 370 0 c +347 0 l +83 0 l +83 702 l +348 702 l +405 702 448 694 477 679 c +482 676 487 673 493 670 c +541 637 565 591 566 531 c +566 470 542 424 494 394 c +462 380 l +456 377 449 375 442 374 c +442 372 l +501 359 543 327 568 276 c +581 251 588 224 588 194 c + +479 520 m +479 585 443 621 370 630 c +361 631 351 631 341 631 c +171 631 l +171 401 l +317 401 l +425 401 479 441 479 520 c + +500 198 m +500 241 485 276 454 303 c +453 304 451 306 449 307 c +426 325 389 334 338 334 c +171 334 l +171 75 l +343 75 l +432 75 483 105 497 166 c +499 175 500 186 500 198 c + +ce} _d +/C{634 0 43 -10 589 713 sc +589 219 m +558 90 490 16 386 -5 c +367 -8 348 -10 329 -10 c +224 -10 146 38 94 135 c +60 199 43 273 43 358 c +43 471 72 560 131 626 c +183 684 251 713 336 713 c +435 713 508 669 555 582 c +570 554 582 523 589 488 c +506 473 l +479 573 430 628 359 637 c +350 638 342 639 333 639 c +248 639 190 590 159 492 c +146 449 139 402 139 351 c +139 261 158 189 197 134 c +230 87 274 63 329 63 c +418 63 477 117 506 225 c +507 229 508 233 509 237 c +589 219 l + +ce} _d +/D{675 0 87 0 636 702 sc +636 353 m +636 247 607 160 550 93 c +496 31 425 0 338 0 c +87 0 l +87 702 l +309 702 l +394 702 462 682 512 642 c +525 631 538 618 551 603 c +608 537 636 454 636 353 c + +547 353 m +547 436 525 504 482 557 c +449 599 403 623 344 628 c +304 629 l +175 629 l +175 75 l +304 75 l +382 75 440 97 478 140 c +484 147 490 155 496 164 c +530 216 547 279 547 353 c + +ce} _d +/E{573 0 87 0 542 702 sc +542 0 m +87 0 l +87 702 l +531 702 l +531 627 l +175 627 l +175 403 l +458 403 l +458 333 l +175 333 l +175 76 l +542 76 l +542 0 l + +ce} _d +/F{491 0 84 0 514 702 sc +514 627 m +172 627 l +172 403 l +456 403 l +456 333 l +172 333 l +172 0 l +84 0 l +84 702 l +514 702 l +514 627 l + +ce} _d +/G{675 0 49 -10 614 713 sc +614 -5 m +560 -5 l +537 82 l +497 24 436 -7 355 -10 c +350 -10 346 -10 342 -10 c +237 -10 157 35 104 125 c +67 187 49 260 49 344 c +49 452 77 541 133 610 c +188 679 262 713 353 713 c +457 713 532 670 579 585 c +591 563 600 538 607 511 c +524 490 l +512 553 480 597 428 622 c +403 633 376 639 347 639 c +256 639 195 589 164 488 c +151 447 144 400 144 348 c +144 251 167 176 212 123 c +247 82 292 61 348 61 c +418 61 469 88 500 141 c +516 170 524 203 524 242 c +524 281 l +349 281 l +349 354 l +614 354 l +614 -5 l + +ce} _d +/H{675 0 83 0 585 702 sc +585 0 m +497 0 l +497 334 l +171 334 l +171 0 l +83 0 l +83 702 l +171 702 l +171 411 l +497 411 l +497 702 l +585 702 l +585 0 l + +ce} _d +/I{266 0 88 0 176 702 sc +176 0 m +88 0 l +88 702 l +176 702 l +176 0 l + +ce} _d +/J{409 0 17 -10 331 702 sc +331 229 m +331 104 290 29 207 2 c +182 -6 153 -10 120 -10 c +86 -10 52 -5 17 4 c +17 76 l +50 69 81 66 108 66 c +171 66 211 83 227 118 c +238 139 243 176 243 229 c +243 702 l +331 702 l +331 229 l + +ce} _d +/K{634 0 88 0 643 702 sc +643 0 m +546 0 l +337 387 l +176 187 l +176 0 l +88 0 l +88 702 l +176 702 l +176 295 l +493 702 l +588 702 l +398 457 l +643 0 l + +ce} _d +/L{512 0 86 0 501 702 sc +501 0 m +86 0 l +86 702 l +174 702 l +174 78 l +501 78 l +501 0 l + +ce} _d +/M{839 0 82 0 761 702 sc +761 0 m +673 0 l +673 613 l +669 613 l +446 0 l +387 0 l +160 613 l +156 613 l +156 0 l +82 0 l +82 702 l +213 702 l +425 140 l +632 702 l +761 702 l +761 0 l + +ce} _d +/N{675 0 84 0 601 702 sc +601 0 m +515 0 l +158 612 l +158 0 l +84 0 l +84 702 l +195 702 l +527 130 l +527 702 l +601 702 l +601 0 l + +ce} _d +/O{675 0 45 -10 623 713 sc +623 359 m +623 258 598 173 548 102 c +495 27 424 -10 335 -10 c +230 -10 151 36 98 128 c +63 189 45 262 45 346 c +45 453 71 540 124 609 c +176 678 246 713 334 713 c +435 713 513 669 566 580 c +604 517 623 443 623 359 c + +530 354 m +530 449 509 522 468 575 c +435 618 392 640 337 640 c +250 640 191 591 159 492 c +144 448 137 399 137 345 c +137 258 157 187 197 133 c +232 85 278 61 335 61 c +417 61 474 108 507 203 c +522 248 530 299 530 354 c + +ce} _d +/P{573 0 66 0 542 702 sc +542 492 m +542 436 523 388 485 348 c +480 343 l +441 305 385 286 311 286 c +154 286 l +154 0 l +66 0 l +66 702 l +301 702 l +376 702 433 686 470 655 c +510 622 534 575 541 515 c +542 507 542 499 542 492 c + +453 492 m +453 557 425 600 370 620 c +351 626 331 629 308 629 c +154 629 l +154 358 l +302 358 l +375 358 422 384 442 435 c +449 452 453 471 453 492 c + +ce} _d +/Q{675 0 42 -167 626 713 sc +626 351 m +626 244 599 158 546 91 c +509 46 462 16 407 1 c +407 -14 l +407 -55 425 -80 461 -88 c +468 -90 476 -91 485 -91 c +500 -91 532 -89 580 -84 c +580 -154 l +553 -163 528 -167 504 -167 c +412 -167 357 -130 340 -55 c +337 -41 335 -26 335 -10 c +224 -7 142 41 91 135 c +58 195 42 265 42 346 c +42 456 69 545 124 613 c +177 680 247 713 334 713 c +441 713 520 666 572 572 c +608 509 626 436 626 351 c + +530 349 m +530 482 496 569 428 610 c +401 627 370 635 333 635 c +234 635 172 576 147 459 c +140 424 136 386 136 344 c +136 239 160 161 207 112 c +238 79 278 62 327 62 c +425 62 488 116 516 225 c +525 263 530 304 530 349 c + +ce} _d +/R{634 0 83 0 588 702 sc +588 0 m +496 0 l +366 304 l +171 304 l +171 0 l +83 0 l +83 702 l +346 702 l +436 702 502 676 544 624 c +563 599 575 568 580 532 c +581 524 581 515 581 506 c +581 445 559 396 515 359 c +496 344 474 333 450 326 c +588 0 l + +493 507 m +493 577 454 616 377 625 c +367 626 357 627 346 627 c +171 627 l +171 376 l +336 376 l +422 376 472 406 487 467 c +491 479 493 492 493 507 c + +ce} _d +/S{634 0 43 -10 590 713 sc +590 201 m +590 114 550 53 469 17 c +428 -1 381 -10 328 -10 c +184 -10 89 56 43 189 c +122 207 l +143 134 191 89 266 72 c +286 67 307 65 330 65 c +398 65 447 83 476 120 c +491 139 499 162 499 189 c +499 237 469 273 408 296 c +343 314 l +264 334 221 345 214 348 c +179 359 153 373 135 389 c +97 423 78 466 78 519 c +78 599 115 655 189 688 c +227 705 269 713 316 713 c +413 713 485 678 534 608 c +554 571 l +559 562 563 552 566 541 c +486 519 l +477 565 448 600 398 624 c +371 637 343 643 314 643 c +265 643 226 629 195 601 c +174 582 164 558 164 531 c +164 485 193 451 250 430 c +264 425 308 414 383 397 c +450 382 497 363 524 340 c +568 305 590 258 590 201 c + +ce} _d +/T{512 0 11 0 499 702 sc +499 626 m +299 626 l +299 0 l +211 0 l +211 626 l +11 626 l +11 702 l +499 702 l +499 626 l + +ce} _d +/U{655 0 83 -10 567 702 sc +567 258 m +567 184 556 128 534 91 c +528 83 522 75 516 68 c +473 16 410 -10 327 -10 c +210 -10 135 32 103 117 c +90 154 83 201 83 258 c +83 702 l +171 702 l +171 258 l +171 179 187 126 218 99 c +243 78 282 68 334 68 c +417 68 468 103 485 174 c +491 197 494 225 494 258 c +494 702 l +567 702 l +567 258 l + +ce} _d +/V{573 0 10 0 569 702 sc +569 702 m +332 0 l +248 0 l +10 702 l +102 702 l +298 123 l +493 702 l +569 702 l + +ce} _d +/W{839 0 11 0 831 702 sc +831 702 m +663 0 l +574 0 l +422 545 l +279 0 l +189 0 l +11 702 l +101 702 l +242 133 l +244 133 l +392 702 l +463 702 l +623 133 l +625 133 l +755 702 l +831 702 l + +ce} _d +/X{552 0 9 0 552 702 sc +552 0 m +453 0 l +275 299 l +91 0 l +9 0 l +234 364 l +34 702 l +132 702 l +287 442 l +443 702 l +524 702 l +328 381 l +552 0 l + +ce} _d +/Y{573 0 11 0 568 702 sc +568 702 m +334 296 l +334 0 l +246 0 l +246 296 l +11 702 l +114 702 l +300 379 l +488 702 l +568 702 l + +ce} _d +/Z{552 0 17 0 513 702 sc +513 0 m +17 0 l +17 76 l +399 630 l +35 630 l +35 702 l +502 702 l +502 644 l +116 76 l +513 76 l +513 0 l + +ce} _d +/a{552 0 56 -10 509 534 sc +509 0 m +429 0 l +420 97 l +385 26 324 -10 238 -10 c +171 -10 120 11 87 53 c +66 79 56 110 56 147 c +56 198 78 240 122 273 c +131 281 142 287 153 292 c +198 313 272 324 375 323 c +420 323 l +420 345 l +420 409 397 448 352 463 c +316 470 l +307 471 298 471 288 471 c +205 471 158 440 145 379 c +71 391 l +80 458 124 502 205 522 c +233 530 263 534 295 534 c +384 534 442 510 469 463 c +488 431 498 380 498 311 c +498 110 l +498 63 502 26 509 0 c + +420 228 m +420 262 l +353 262 l +212 262 142 222 142 143 c +142 100 165 71 211 58 c +224 54 239 52 255 52 c +312 52 356 76 388 123 c +409 154 420 189 420 228 c + +ce} _d +/e{552 0 38 -10 506 534 sc +506 254 m +128 254 l +127 187 143 136 174 101 c +200 72 236 57 283 57 c +341 57 384 82 411 133 c +415 140 419 149 422 158 c +498 142 l +479 82 439 38 378 11 c +346 -3 312 -10 276 -10 c +187 -10 121 27 78 100 c +51 145 38 199 38 260 c +38 346 64 415 116 468 c +159 512 213 534 279 534 c +369 534 434 495 473 417 c +495 374 506 323 506 266 c +506 254 l + +418 313 m +419 370 401 413 366 442 c +342 462 313 472 279 472 c +226 472 186 448 158 400 c +142 374 133 345 132 313 c +418 313 l + +ce} _d +/i{245 0 79 0 167 702 sc +167 612 m +79 612 l +79 702 l +167 702 l +167 612 l + +163 0 m +83 0 l +83 520 l +163 520 l +163 0 l + +ce} _d +/n{552 0 71 0 482 534 sc +482 0 m +402 0 l +402 322 l +402 366 398 396 391 412 c +390 414 389 416 388 418 c +373 444 348 459 313 464 c +308 465 304 465 299 465 c +234 465 190 432 166 366 c +156 337 151 306 151 272 c +151 0 l +71 0 l +71 520 l +146 520 l +146 424 l +148 424 l +173 477 211 511 263 526 c +278 531 294 534 310 534 c +375 534 423 508 455 457 c +458 453 l +474 425 482 377 482 309 c +482 0 l + +ce} _d +/u{552 0 71 -10 479 520 sc +479 0 m +407 0 l +407 103 l +404 103 l +386 58 355 25 310 4 c +289 -5 268 -10 245 -10 c +144 -10 87 45 74 156 c +72 171 71 187 71 204 c +71 520 l +151 520 l +151 204 l +151 113 181 65 242 59 c +242 59 245 59 252 59 c +311 59 354 89 380 148 c +393 177 399 209 399 244 c +399 520 l +479 520 l +479 0 l + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /WenQuanYiZenHeiMono def +/PaintType 0 def +/FontMatrix [0.0009765625 0 0 0.0009765625 0 0] def +/FontBBox [-129 -304 1076 986] def +/FontType 3 def +/Encoding [/space /colon /A /B /C /D /E /F /G /H /I /J /K /L /M /N /O /P /Q /R /S /T /U /V /W /X /Y /Z /a /e /i /n /o /u] def +/CharStrings 35 dict dup begin +/.notdef 0 def +/space{512 0 0 0 0 0 sc +ce} _d +/colon{512 0 195 18 317 571 sc +195 418 m +195 571 l +317 571 l +317 418 l +195 418 l + +195 18 m +195 172 l +317 172 l +317 18 l +195 18 l + +ce} _d +/A{512 0 20 18 492 766 sc +255 694 m +253 694 l +168 305 l +340 305 l +255 694 l + +356 233 m +152 233 l +104 18 l +20 18 l +205 766 l +307 766 l +492 18 l +403 18 l +356 233 l + +ce} _d +/B{512 0 77 8 466 776 sc +161 459 m +207 459 l +262 459 301 469 326 489 c +351 509 364 540 364 582 c +364 621 352 651 327 672 c +302 694 267 705 222 705 c +197 705 177 702 161 696 c +161 459 l + +161 387 m +161 88 l +186 83 217 80 253 80 c +339 80 382 135 382 244 c +382 339 327 387 217 387 c +161 387 l + +466 233 m +466 83 389 8 236 8 c +181 8 128 13 77 24 c +77 761 l +128 771 181 776 236 776 c +375 776 445 715 445 592 c +445 550 435 515 414 486 c +393 457 365 438 328 429 c +328 427 l +367 420 400 398 426 361 c +453 325 466 282 466 233 c + +ce} _d +/C{512 0 56 8 435 776 sc +56 392 m +56 527 77 624 118 685 c +159 746 221 776 302 776 c +347 776 390 766 430 745 c +430 669 l +389 691 348 702 307 702 c +194 702 138 599 138 392 c +138 280 152 200 181 153 c +210 106 252 82 307 82 c +350 82 392 95 435 121 c +435 39 l +395 18 351 8 302 8 c +219 8 157 37 116 96 c +76 155 56 253 56 392 c + +ce} _d +/D{512 0 67 8 476 776 sc +392 392 m +392 507 377 587 347 633 c +318 679 271 702 207 702 c +184 702 166 699 151 694 c +151 90 l +166 85 184 82 207 82 c +251 82 286 90 311 107 c +337 124 357 155 371 200 c +385 246 392 310 392 392 c + +476 392 m +476 251 454 151 411 94 c +368 37 300 8 207 8 c +159 8 112 13 67 24 c +67 761 l +112 771 159 776 207 776 c +300 776 368 747 411 688 c +454 630 476 531 476 392 c + +ce} _d +/E{512 0 82 18 430 766 sc +166 692 m +166 459 l +420 459 l +420 387 l +166 387 l +166 92 l +430 92 l +430 18 l +82 18 l +82 766 l +430 766 l +430 692 l +166 692 l + +ce} _d +/F{512 0 92 18 430 766 sc +176 387 m +176 18 l +92 18 l +92 766 l +430 766 l +430 692 l +176 692 l +176 459 l +420 459 l +420 387 l +176 387 l + +ce} _d +/G{512 0 41 8 461 776 sc +379 105 m +379 387 l +220 387 l +220 459 l +461 459 l +461 49 l +406 22 348 8 287 8 c +206 8 145 38 103 99 c +62 160 41 257 41 392 c +41 526 63 623 106 684 c +149 745 215 776 302 776 c +342 776 386 768 435 751 c +435 672 l +390 692 346 702 302 702 c +243 702 198 677 167 628 c +136 579 121 501 121 392 c +121 185 178 82 292 82 c +323 82 352 90 379 105 c + +ce} _d +/H{512 0 61 18 451 766 sc +143 766 m +143 461 l +365 461 l +365 766 l +451 766 l +451 18 l +365 18 l +365 387 l +143 387 l +143 18 l +61 18 l +61 766 l +143 766 l + +ce} _d +/I{512 0 92 18 420 766 sc +420 18 m +92 18 l +92 90 l +213 90 l +213 694 l +92 694 l +92 766 l +420 766 l +420 694 l +299 694 l +299 90 l +420 90 l +420 18 l + +ce} _d +/J{512 0 61 8 410 766 sc +410 766 m +410 213 l +410 138 395 85 365 54 c +336 23 286 8 215 8 c +159 8 108 18 61 39 c +61 128 l +81 117 107 106 138 96 c +170 87 196 82 215 82 c +251 82 278 92 296 113 c +314 134 323 168 323 215 c +323 694 l +154 694 l +154 766 l +410 766 l + +ce} _d +/K{512 0 77 18 476 766 sc +161 428 m +163 428 l +374 766 l +471 766 l +241 408 l +476 18 l +379 18 l +163 387 l +161 387 l +161 18 l +77 18 l +77 766 l +161 766 l +161 428 l + +ce} _d +/L{512 0 102 18 430 766 sc +186 766 m +186 92 l +430 92 l +430 18 l +102 18 l +102 766 l +186 766 l + +ce} _d +/M{512 0 41 18 471 766 sc +387 571 m +385 571 l +295 223 l +213 223 l +123 571 l +121 571 l +121 18 l +41 18 l +41 766 l +135 766 l +257 305 l +259 305 l +381 766 l +471 766 l +471 18 l +387 18 l +387 571 l + +ce} _d +/N{512 0 67 18 445 766 sc +155 582 m +153 582 l +153 18 l +67 18 l +67 766 l +153 766 l +361 203 l +364 203 l +364 766 l +445 766 l +445 18 l +364 18 l +155 582 l + +ce} _d +/O{512 0 41 8 471 776 sc +93 689 m +128 747 183 776 256 776 c +329 776 383 747 418 689 c +453 632 471 533 471 392 c +471 251 453 152 418 94 c +383 37 329 8 256 8 c +183 8 128 37 93 94 c +58 152 41 251 41 392 c +41 533 58 632 93 689 c + +183 108 m +202 91 226 82 256 82 c +286 82 310 91 328 108 c +347 125 361 157 372 203 c +383 250 389 313 389 392 c +389 471 383 534 372 580 c +361 627 347 659 328 676 c +310 693 286 702 256 702 c +226 702 202 693 183 676 c +165 659 150 627 139 580 c +128 534 123 471 123 392 c +123 313 128 250 139 203 c +150 157 165 125 183 108 c + +ce} _d +/P{512 0 77 18 466 776 sc +384 551 m +384 605 372 644 347 668 c +322 693 284 705 232 705 c +204 705 180 702 161 696 c +161 397 l +181 394 205 392 232 392 c +285 392 323 404 347 428 c +372 453 384 494 384 551 c + +466 551 m +466 469 448 410 412 374 c +376 339 320 321 243 321 c +219 321 192 323 161 326 c +161 18 l +77 18 l +77 761 l +130 771 185 776 241 776 c +318 776 375 758 411 722 c +448 687 466 630 466 551 c + +ce} _d +/Q{512 0 41 -135 492 776 sc +93 689 m +128 747 183 776 256 776 c +329 776 383 747 418 689 c +453 632 471 533 471 392 c +471 206 437 89 369 40 c +369 38 l +397 23 422 1 443 -30 c +465 -61 481 -96 492 -135 c +401 -135 l +387 -82 369 -44 346 -23 c +323 -2 293 8 256 8 c +183 8 128 37 93 94 c +58 152 41 251 41 392 c +41 533 58 632 93 689 c + +183 108 m +202 91 226 82 256 82 c +286 82 310 91 328 108 c +347 125 361 157 372 203 c +383 250 389 313 389 392 c +389 471 383 534 372 580 c +361 627 347 659 328 676 c +310 693 286 702 256 702 c +226 702 202 693 183 676 c +165 659 150 627 139 580 c +128 534 123 471 123 392 c +123 313 128 250 139 203 c +150 157 165 125 183 108 c + +ce} _d +/R{512 0 72 18 481 776 sc +379 571 m +379 660 328 705 227 705 c +199 705 175 702 156 696 c +156 418 l +217 418 l +276 418 318 429 342 452 c +367 475 379 514 379 571 c + +156 346 m +156 18 l +72 18 l +72 761 l +125 771 180 776 236 776 c +312 776 368 759 405 725 c +442 692 461 640 461 571 c +461 474 424 408 349 374 c +349 372 l +370 361 393 317 417 238 c +481 18 l +393 18 l +333 240 l +321 283 307 312 290 325 c +274 339 246 346 207 346 c +156 346 l + +ce} _d +/S{512 0 72 8 451 776 sc +266 702 m +234 702 208 692 187 671 c +166 650 156 624 156 592 c +156 558 163 530 178 507 c +193 485 217 466 251 451 c +327 417 379 382 408 345 c +437 309 451 262 451 203 c +451 138 433 90 398 57 c +363 24 312 8 246 8 c +184 8 128 27 77 65 c +77 162 l +132 109 190 82 251 82 c +328 82 367 122 367 203 c +367 240 358 271 340 296 c +322 321 292 342 251 361 c +187 390 141 422 113 459 c +86 496 72 540 72 592 c +72 647 89 691 124 725 c +159 759 204 776 261 776 c +297 776 327 773 350 768 c +373 763 400 752 430 735 c +430 643 l +377 682 323 702 266 702 c + +ce} _d +/T{512 0 56 18 456 766 sc +214 18 m +214 694 l +56 694 l +56 766 l +456 766 l +456 694 l +298 694 l +298 18 l +214 18 l + +ce} _d +/U{512 0 61 8 451 766 sc +402 58 m +369 25 321 8 256 8 c +191 8 143 25 110 58 c +77 91 61 143 61 213 c +61 766 l +147 766 l +147 233 l +147 178 156 139 174 116 c +193 93 221 82 258 82 c +295 82 323 93 341 116 c +360 139 369 178 369 233 c +369 766 l +451 766 l +451 213 l +451 143 435 91 402 58 c + +ce} _d +/V{512 0 31 18 481 766 sc +259 90 m +397 766 l +481 766 l +307 18 l +205 18 l +31 766 l +119 766 l +257 90 l +259 90 l + +ce} _d +/W{512 0 26 18 486 766 sc +157 141 m +159 141 l +214 664 l +306 664 l +361 141 l +364 141 l +410 766 l +486 766 l +425 18 l +313 18 l +257 551 l +255 551 l +199 18 l +87 18 l +26 766 l +111 766 l +157 141 l + +ce} _d +/X{512 0 51 18 461 766 sc +257 469 m +259 469 l +374 766 l +459 766 l +307 402 l +461 18 l +369 18 l +255 331 l +253 331 l +138 18 l +51 18 l +205 402 l +53 766 l +143 766 l +257 469 l + +ce} _d +/Y{512 0 31 18 481 766 sc +257 402 m +259 402 l +394 766 l +481 766 l +298 315 l +298 18 l +214 18 l +214 315 l +31 766 l +123 766 l +257 402 l + +ce} _d +/Z{512 0 77 18 435 766 sc +343 692 m +343 694 l +77 694 l +77 766 l +435 766 l +435 694 l +169 92 l +169 90 l +435 90 l +435 18 l +77 18 l +77 90 l +343 692 l + +ce} _d +/a{512 0 61 8 440 561 sc +261 561 m +330 561 377 547 402 520 c +427 493 440 442 440 367 c +440 18 l +367 18 l +365 95 l +362 95 l +330 37 279 8 210 8 c +165 8 129 22 102 50 c +75 79 61 118 61 167 c +61 229 82 277 124 310 c +166 344 229 361 312 361 c +361 361 l +361 387 l +361 426 353 453 338 469 c +323 485 298 493 261 493 c +238 493 210 489 175 480 c +140 472 111 463 87 452 c +87 525 l +111 536 140 544 174 551 c +208 558 237 561 261 561 c + +361 300 m +312 300 l +196 300 138 257 138 172 c +138 141 146 118 161 101 c +177 85 198 77 225 77 c +266 77 298 93 323 126 c +348 159 361 205 361 264 c +361 300 l + +ce} _d +/e{512 0 56 8 445 561 sc +141 258 m +144 195 157 149 180 121 c +203 93 237 79 282 79 c +323 79 369 91 420 116 c +420 34 l +369 17 321 8 276 8 c +129 8 56 100 56 285 c +56 381 73 451 107 495 c +142 539 193 561 261 561 c +323 561 369 540 399 497 c +430 455 445 386 445 290 c +445 283 444 272 443 258 c +141 258 l + +141 326 m +365 326 l +364 435 330 490 261 490 c +222 490 193 478 174 453 c +155 429 144 387 141 326 c + +ce} _d +/i{512 0 97 18 435 797 sc +324 551 m +324 88 l +435 88 l +435 18 l +97 18 l +97 88 l +240 88 l +240 481 l +128 481 l +128 551 l +324 551 l + +219 674 m +219 797 l +324 797 l +324 674 l +219 674 l + +ce} _d +/n{512 0 72 18 451 561 sc +451 356 m +451 18 l +372 18 l +372 338 l +372 398 365 438 351 458 c +338 478 313 488 276 488 c +242 488 213 469 189 431 c +165 393 153 341 153 276 c +153 18 l +72 18 l +72 551 l +147 551 l +150 474 l +152 474 l +165 500 184 521 211 537 c +238 553 266 561 297 561 c +351 561 390 545 414 514 c +439 483 451 431 451 356 c + +ce} _d +/o{512 0 51 8 461 561 sc +51 284 m +51 469 119 561 256 561 c +393 561 461 469 461 284 c +461 100 393 8 256 8 c +119 8 51 100 51 284 c + +164 125 m +184 94 215 79 256 79 c +297 79 328 94 347 125 c +367 156 377 209 377 284 c +377 359 367 412 347 443 c +328 474 297 490 256 490 c +215 490 184 474 164 443 c +145 412 135 359 135 284 c +135 209 145 156 164 125 c + +ce} _d +/u{512 0 67 8 435 551 sc +67 203 m +67 551 l +145 551 l +145 221 l +145 164 151 127 164 108 c +177 90 201 81 236 81 c +269 81 297 100 320 137 c +343 175 354 227 354 293 c +354 551 l +435 551 l +435 18 l +359 18 l +357 95 l +355 95 l +342 68 323 46 298 31 c +273 16 245 8 215 8 c +162 8 124 23 101 52 c +78 81 67 132 67 203 c + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +0 0 translate +0 0 504 72 rectclip +gsave +0 0 m +504 0 l +504 72 l +0 72 l +cl +1 setgray +fill +grestore +gsave +0 36 m +504 36 l +504 72 l +0 72 l +0 36 l +cl +grestore +0 setgray +/WenQuanYiZenHei 16.000 selectfont +gsave + +55.4375 49.7812 translate +0 rotate +0 0 m /W glyphshow +13.1094 0 m /e glyphshow +21.7344 0 m /n glyphshow +30.3594 0 m /Q glyphshow +40.9062 0 m /u glyphshow +49.5312 0 m /a glyphshow +58.1562 0 m /n glyphshow +66.7812 0 m /Y glyphshow +75.7344 0 m /i glyphshow +79.5625 0 m /space glyphshow +84.3594 0 m /Z glyphshow +92.9844 0 m /e glyphshow +101.609 0 m /n glyphshow +110.234 0 m /space glyphshow +115.031 0 m /H glyphshow +125.578 0 m /e glyphshow +134.203 0 m /i glyphshow +138.031 0 m /colon glyphshow +142.828 0 m /space glyphshow +147.625 0 m /A glyphshow +156.578 0 m /B glyphshow +166.484 0 m /C glyphshow +176.391 0 m /D glyphshow +186.938 0 m /E glyphshow +195.891 0 m /F glyphshow +203.562 0 m /G glyphshow +214.109 0 m /H glyphshow +224.656 0 m /I glyphshow +228.812 0 m /J glyphshow +235.203 0 m /K glyphshow +245.109 0 m /L glyphshow +253.109 0 m /M glyphshow +266.219 0 m /N glyphshow +276.766 0 m /O glyphshow +287.312 0 m /P glyphshow +296.266 0 m /Q glyphshow +306.812 0 m /R glyphshow +316.719 0 m /S glyphshow +326.625 0 m /T glyphshow +334.625 0 m /U glyphshow +344.859 0 m /V glyphshow +353.812 0 m /W glyphshow +366.922 0 m /X glyphshow +375.547 0 m /Y glyphshow +384.5 0 m /Z glyphshow +grestore +gsave +0 0 m +504 0 l +504 36 l +0 36 l +0 0 l +cl +grestore +/WenQuanYiZenHeiMono 16.000 selectfont +gsave + +52 13.6328 translate +0 rotate +0 0 m /W glyphshow +8 0 m /e glyphshow +16 0 m /n glyphshow +24 0 m /Q glyphshow +32 0 m /u glyphshow +40 0 m /a glyphshow +48 0 m /n glyphshow +56 0 m /Y glyphshow +64 0 m /i glyphshow +72 0 m /space glyphshow +80 0 m /Z glyphshow +88 0 m /e glyphshow +96 0 m /n glyphshow +104 0 m /space glyphshow +112 0 m /H glyphshow +120 0 m /e glyphshow +128 0 m /i glyphshow +136 0 m /space glyphshow +144 0 m /M glyphshow +152 0 m /o glyphshow +160 0 m /n glyphshow +168 0 m /o glyphshow +176 0 m /colon glyphshow +184 0 m /space glyphshow +192 0 m /A glyphshow +200 0 m /B glyphshow +208 0 m /C glyphshow +216 0 m /D glyphshow +224 0 m /E glyphshow +232 0 m /F glyphshow +240 0 m /G glyphshow +248 0 m /H glyphshow +256 0 m /I glyphshow +264 0 m /J glyphshow +272 0 m /K glyphshow +280 0 m /L glyphshow +288 0 m /M glyphshow +296 0 m /N glyphshow +304 0 m /O glyphshow +312 0 m /P glyphshow +320 0 m /Q glyphshow +328 0 m /R glyphshow +336 0 m /S glyphshow +344 0 m /T glyphshow +352 0 m /U glyphshow +360 0 m /V glyphshow +368 0 m /W glyphshow +376 0 m /X glyphshow +384 0 m /Y glyphshow +392 0 m /Z glyphshow +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps deleted file mode 100644 index 9c9645b47cf0..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps +++ /dev/null @@ -1,112 +0,0 @@ -%!PS-Adobe-3.0 EPSF-3.0 -%%Orientation: portrait -%%BoundingBox: 18.0 180.0 594.0 612.0 -%%EndComments -%%BeginProlog -/mpldict 11 dict def -mpldict begin -/d { bind def } bind def -/m { moveto } d -/l { lineto } d -/r { rlineto } d -/c { curveto } d -/cl { closepath } d -/ce { closepath eofill } d -/box { - m - 1 index 0 r - 0 exch r - neg 0 r - cl - } d -/clipbox { - box - clip - newpath - } d -/sc { setcachedevice } d -%!PS-Adobe-3.0 Resource-Font -%%Creator: Converted from TrueType to Type 3 by Matplotlib. -10 dict begin -/FontName /DejaVuSans def -/PaintType 0 def -/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def -/FontBBox [-2090 -948 3673 2524] def -/FontType 3 def -/Encoding [/I /J /slash] def -/CharStrings 4 dict dup begin -/.notdef 0 def -/I{604 0 201 0 403 1493 sc -201 1493 m -403 1493 l -403 0 l -201 0 l -201 1493 l - -ce} d -/J{604 0 -106 -410 403 1493 sc -201 1493 m -403 1493 l -403 104 l -403 -76 369 -207 300 -288 c -232 -369 122 -410 -29 -410 c --106 -410 l --106 -240 l --43 -240 l -46 -240 109 -215 146 -165 c -183 -115 201 -25 201 104 c -201 1493 l - -ce} d -/slash{690 0 0 -190 690 1493 sc -520 1493 m -690 1493 l -170 -190 l -0 -190 l -520 1493 l - -ce} d -end readonly def - -/BuildGlyph { - exch begin - CharStrings exch - 2 copy known not {pop /.notdef} if - true 3 1 roll get exec - end -} d - -/BuildChar { - 1 index /Encoding get exch get - 1 index /BuildGlyph get exec -} d - -FontName currentdict end definefont pop -end -%%EndProlog -mpldict begin -18 180 translate -576 432 0 0 clipbox -gsave -0 0 m -576 0 l -576 432 l -0 432 l -cl -1.000 setgray -fill -grestore -0.000 setgray -/DejaVuSans findfont -12.000 scalefont -setfont -gsave -288.000000 216.000000 translate -0.000000 rotate -0.000000 0 m /I glyphshow -3.539062 0 m /slash glyphshow -7.582031 0 m /J glyphshow -grestore - -end -showpage diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg index 6967f80a1186..a7195c665c14 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:36.846948 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,721 +26,752 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png index 0317cb99e1c0..c2076843da4a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg index 9d57faac5f18..09dd81f56563 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:33.031781 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,297 +26,317 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg index 90f9b2cec969..b6236288603d 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.551077 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,437 +26,467 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png index d6802f84bfda..5a615d92e166 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg index 77ded780c3f1..4d7fdcc30954 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.658346 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,268 +26,288 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - + - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg index 7a7b7ec42c25..e73b1c5e872b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:39.585869 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,190 +26,202 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - + - - - + - - - + - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg index 0846b552246a..ca0439485b08 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.642097 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,115 +26,121 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - - + - - - - - - +M 1381 2969 +Q 1594 3256 1914 3420 +Q 2234 3584 2584 3584 +Q 3122 3584 3439 3221 +Q 3756 2859 3756 2241 +Q 3756 1734 3570 1259 +Q 3384 784 3041 416 +Q 2816 172 2522 40 +Q 2228 -91 1906 -91 +Q 1566 -91 1316 65 +Q 1066 222 909 531 +L 806 0 +L 231 0 +L 1178 4863 +L 1753 4863 +L 1381 2969 +z +" transform="scale(0.015625)"/> + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg index 24db824fd37c..8287a2338258 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.896681 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,103 +26,109 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - + + - + - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg index 189491319c10..0bbef213526f 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:39.508765 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,209 +26,220 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + + - + - - - + - - + - - - - - - - - - - - - - +M 1959 2075 +Q 2384 2075 2632 2365 +Q 2881 2656 2881 3163 +Q 2881 3666 2632 3958 +Q 2384 4250 1959 4250 +Q 1534 4250 1286 3958 +Q 1038 3666 1038 3163 +Q 1038 2656 1286 2365 +Q 1534 2075 1959 2075 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg index e0721c9e47a4..3e7c6dc1c42c 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.642033 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,493 +26,524 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png index b405dd438309..c8bae48383aa 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.svg index 13ec8213ff31..ced57fed3b2b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.725227 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,278 +26,298 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - + - - - + - - + - - - - - - - - - - - - - - - - - - - - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg index a4fb4be582a4..cd7dfc34183b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:42.172241 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,223 +26,234 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - + - - - + - - + - + - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg index 4623754e2963..045cc829e0cf 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.874726 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,531 +26,562 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png index 1923648b80a3..d0233a4ee7a0 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.svg index bc6756feadc8..8184565aaf2a 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.959357 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,284 +26,304 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg index d61317816ad6..c3dd8722b044 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.040182 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,435 +26,466 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png index a86119004e62..6f816c2ee723 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg index 4e129aa6c87d..50bdb38d37b1 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.119948 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,269 +26,289 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - + - - + - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png b/lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png new file mode 100644 index 000000000000..3dad1f9a19f7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf deleted file mode 100644 index 8890790d2ea2..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf deleted file mode 100644 index a8ce9fca346c..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_kerning.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_kerning.pdf deleted file mode 100644 index 7db9a1b44fad..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_kerning.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/test_afm.py b/lib/matplotlib/tests/test_afm.py index 80cf8ac60feb..bc1d587baf6b 100644 --- a/lib/matplotlib/tests/test_afm.py +++ b/lib/matplotlib/tests/test_afm.py @@ -47,20 +47,20 @@ def test_parse_header(): fh = BytesIO(AFM_TEST_DATA) header = _afm._parse_header(fh) assert header == { - b'StartFontMetrics': 2.0, - b'FontName': 'MyFont-Bold', - b'EncodingScheme': 'FontSpecific', - b'FullName': 'My Font Bold', - b'FamilyName': 'Test Fonts', - b'Weight': 'Bold', - b'ItalicAngle': 0.0, - b'IsFixedPitch': False, - b'UnderlinePosition': -100, - b'UnderlineThickness': 56.789, - b'Version': '001.000', - b'Notice': b'Copyright \xa9 2017 No one.', - b'FontBBox': [0, -321, 1234, 369], - b'StartCharMetrics': 3, + 'StartFontMetrics': 2.0, + 'FontName': 'MyFont-Bold', + 'EncodingScheme': 'FontSpecific', + 'FullName': 'My Font Bold', + 'FamilyName': 'Test Fonts', + 'Weight': 'Bold', + 'ItalicAngle': 0.0, + 'IsFixedPitch': False, + 'UnderlinePosition': -100, + 'UnderlineThickness': 56.789, + 'Version': '001.000', + 'Notice': b'Copyright \xa9 2017 No one.', + 'FontBBox': [0, -321, 1234, 369], + 'StartCharMetrics': 3, } @@ -69,20 +69,23 @@ def test_parse_char_metrics(): _afm._parse_header(fh) # position metrics = _afm._parse_char_metrics(fh) assert metrics == ( - {0: (250.0, 'space', [0, 0, 0, 0]), - 42: (1141.0, 'foo', [40, 60, 800, 360]), - 99: (583.0, 'bar', [40, -10, 543, 210]), - }, - {'space': (250.0, 'space', [0, 0, 0, 0]), - 'foo': (1141.0, 'foo', [40, 60, 800, 360]), - 'bar': (583.0, 'bar', [40, -10, 543, 210]), - }) + { + 0: _afm.CharMetrics(250.0, 'space', (0, 0, 0, 0)), + 42: _afm.CharMetrics(1141.0, 'foo', (40, 60, 800, 360)), + 99: _afm.CharMetrics(583.0, 'bar', (40, -10, 543, 210)), + }, + { + 'space': _afm.CharMetrics(250.0, 'space', (0, 0, 0, 0)), + 'foo': _afm.CharMetrics(1141.0, 'foo', (40, 60, 800, 360)), + 'bar': _afm.CharMetrics(583.0, 'bar', (40, -10, 543, 210)), + } + ) def test_get_familyname_guessed(): fh = BytesIO(AFM_TEST_DATA) font = _afm.AFM(fh) - del font._header[b'FamilyName'] # remove FamilyName, so we have to guess + del font._header['FamilyName'] # remove FamilyName, so we have to guess assert font.get_familyname() == 'My Font' diff --git a/lib/matplotlib/tests/test_agg_filter.py b/lib/matplotlib/tests/test_agg_filter.py index 545e62d20d7c..4c5b55a3d15c 100644 --- a/lib/matplotlib/tests/test_agg_filter.py +++ b/lib/matplotlib/tests/test_agg_filter.py @@ -5,11 +5,8 @@ @image_comparison(baseline_images=['agg_filter_alpha'], - extensions=['gif', 'png', 'pdf']) + extensions=['gif', 'png', 'pdf'], style='mpl20') def test_agg_filter_alpha(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - ax = plt.axes() x, y = np.mgrid[0:7, 0:8] data = x**2 - y**2 diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index b34dc01e41cb..4ca5c1220972 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -300,6 +300,8 @@ def test_embed_limit(method_name, caplog, anim): and record.levelname == "WARNING") +@pytest.mark.skipif(sys.platform == 'emscripten', + reason='emscripten does not support subprocesses') @pytest.mark.skipif(shutil.which("/bin/sh") is None, reason="requires a POSIX OS") def test_failing_ffmpeg(tmp_path, monkeypatch, anim): """ diff --git a/lib/matplotlib/tests/test_arrow_patches.py b/lib/matplotlib/tests/test_arrow_patches.py index 9cf1636f7913..08d3d62f0a84 100644 --- a/lib/matplotlib/tests/test_arrow_patches.py +++ b/lib/matplotlib/tests/test_arrow_patches.py @@ -28,7 +28,7 @@ def test_fancyarrow(): ax.tick_params(labelleft=False, labelbottom=False) -@image_comparison(['boxarrow_test_image.png']) +@image_comparison(['boxarrow_test_image.png'], style='mpl20') def test_boxarrow(): styles = mpatches.BoxStyle.get_styles() diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index 6bb50826f7ef..976cbb4feae3 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -217,9 +217,6 @@ def test_remove(): @image_comparison(["default_edges.png"], remove_text=True, style='default') def test_default_edges(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2) ax1.plot(np.arange(10), np.arange(10), 'x', diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 57a295d418a6..2504407767dc 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -239,14 +239,13 @@ def test_matshow(fig_test, fig_ref): ax_ref.xaxis.set_ticks_position('both') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison([f'formatter_ticker_{i:03d}.png' for i in range(1, 6)], - tol=0.02 if platform.machine() == 'x86_64' else 0.04) +@image_comparison([f'formatter_ticker_{i:03d}.png' for i in range(1, 6)], style='mpl20', + tol=0.03 if sys.platform == 'darwin' else 0) def test_formatter_ticker(): import matplotlib.testing.jpl_units as units units.register() - # This should affect the tick size. (Tests issue #543) + # This should not affect the tick size. (Tests issue #543) matplotlib.rcParams['lines.markeredgewidth'] = 30 # This essentially test to see if user specified labels get overwritten @@ -332,7 +331,7 @@ def test_strmethodformatter_auto_formatter(): assert ax.yaxis.get_minor_formatter().fmt == targ_strformatter.fmt -@image_comparison(["twin_axis_locators_formatters.png"]) +@image_comparison(["twin_axis_locators_formatters.png"], style='mpl20') def test_twin_axis_locators_formatters(): vals = np.linspace(0, 1, num=5, endpoint=True) locs = np.sin(np.pi * vals / 2.0) @@ -342,6 +341,7 @@ def test_twin_axis_locators_formatters(): fig = plt.figure() ax1 = fig.add_subplot(1, 1, 1) + ax1.margins(0) ax1.plot([0.1, 100], [0, 1]) ax1.yaxis.set_major_locator(majl) ax1.yaxis.set_minor_locator(minl) @@ -735,7 +735,7 @@ def test_nargs_pcolorfast(): ax.pcolorfast([(0, 1), (0, 2)], [[1, 2, 3], [1, 2, 3]]) -@image_comparison(['offset_points'], remove_text=True) +@image_comparison(['offset_points'], remove_text=True, style='mpl20') def test_basic_annotate(): # Setup some data t = np.arange(0.0, 5.0, 0.01) @@ -810,8 +810,7 @@ def test_annotate_signature(): assert p1 == p2 -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}, tol=0.2) +@image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}, style='mpl20') def test_fill_units(): import matplotlib.testing.jpl_units as units units.register() @@ -876,7 +875,7 @@ def test_errorbar_mapview_kwarg(): ax.errorbar(x=D.keys(), y=D.values(), xerr=D.values()) -@image_comparison(['single_point', 'single_point']) +@image_comparison(['single_point', 'single_point'], style='mpl20') def test_single_point(): # Issue #1796: don't let lines.marker affect the grid matplotlib.rcParams['lines.marker'] = 'o' @@ -935,7 +934,7 @@ def test_aitoff_proj(): ax.plot(X.flat, Y.flat, 'o', markersize=4) -@image_comparison(['axvspan_epoch.png']) +@image_comparison(['axvspan_epoch.png'], style='mpl20') def test_axvspan_epoch(): import matplotlib.testing.jpl_units as units units.register() @@ -950,7 +949,7 @@ def test_axvspan_epoch(): ax.set_xlim(t0 - 5.0*dt, tf + 5.0*dt) -@image_comparison(['axhspan_epoch.png'], tol=0.02) +@image_comparison(['axhspan_epoch.png'], style='mpl20') def test_axhspan_epoch(): import matplotlib.testing.jpl_units as units units.register() @@ -1030,9 +1029,6 @@ def test_hexbin_pickable(): def test_hexbin_log(): # Issue #1636 (and also test log scaled colorbar) - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - np.random.seed(19680801) n = 100000 x = np.random.standard_normal(n) @@ -1151,7 +1147,7 @@ def test_inverted_limits(): assert ax.get_ylim() == (10, 1) -@image_comparison(['nonfinite_limits']) +@image_comparison(['nonfinite_limits'], style='mpl20') def test_nonfinite_limits(): x = np.arange(0., np.e, 0.01) # silence divide by zero warning from log(0) @@ -1345,7 +1341,7 @@ def test_fill_between_interpolate_nan(): # test_symlog and test_symlog2 used to have baseline images in all three # formats, but the png and svg baselines got invalidated by the removal of # minor tick overstriking. -@image_comparison(['symlog.pdf']) +@image_comparison(['symlog.pdf'], style='mpl20') def test_symlog(): x = np.array([0, 1, 2, 4, 6, 9, 12, 24]) y = np.array([1000000, 500000, 100000, 100, 5, 0, 0, 0]) @@ -1517,12 +1513,8 @@ def test_pcolormesh_log_scale(fig_test, fig_ref): ax.set_xscale('log') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20', tol=0.3) +@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20') def test_pcolormesh_datetime_axis(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) base = datetime.datetime(2013, 1, 1) @@ -1546,8 +1538,7 @@ def test_pcolormesh_datetime_axis(): label.set_rotation(30) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['pcolor_datetime_axis.png'], style='mpl20', tol=0.3) +@image_comparison(['pcolor_datetime_axis.png'], style='mpl20') def test_pcolor_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -1757,8 +1748,8 @@ def test_pcolorauto(fig_test, fig_ref, snap): ax.pcolormesh(x2, y2, Z, snap=snap) -@image_comparison(['canonical'], - tol=0 if platform.machine() == 'x86_64' else 0.02) +@image_comparison(['canonical'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.03) def test_canonical(): fig, ax = plt.subplots() ax.plot([1, 2, 3]) @@ -1843,7 +1834,7 @@ def test_marker_as_markerstyle(): ax.errorbar([1, 2, 3], [5, 4, 3], marker=m) -@image_comparison(['markevery.png'], remove_text=True) +@image_comparison(['markevery.png'], remove_text=True, style='mpl20') def test_markevery(): x = np.linspace(0, 10, 100) y = np.sin(x) * np.sqrt(x/10 + 0.5) @@ -1851,27 +1842,23 @@ def test_markevery(): # check marker only plot fig, ax = plt.subplots() ax.plot(x, y, 'o', label='default') - ax.plot(x, y, 'd', markevery=None, label='mark all') - ax.plot(x, y, 's', markevery=10, label='mark every 10') - ax.plot(x, y, '+', markevery=(5, 20), label='mark every 5 starting at 10') + ax.plot(x, y+1, 'd', markevery=None, label='mark all') + ax.plot(x, y+2, 's', markevery=10, label='mark every 10') + ax.plot(x, y+3, '+', markevery=(5, 20), label='mark every 20 starting at 5') ax.legend() -@image_comparison(['markevery_line.png'], remove_text=True, tol=0.005) +@image_comparison(['markevery_line.png'], remove_text=True, style='mpl20') def test_markevery_line(): - # TODO: a slight change in rendering between Inkscape versions may explain - # why one had to introduce a small non-zero tolerance for the SVG test - # to pass. One may try to remove this hack once Travis' Inkscape version - # is modern enough. FWIW, no failure with 0.92.3 on my computer (#11358). x = np.linspace(0, 10, 100) y = np.sin(x) * np.sqrt(x/10 + 0.5) # check line/marker combos fig, ax = plt.subplots() ax.plot(x, y, '-o', label='default') - ax.plot(x, y, '-d', markevery=None, label='mark all') - ax.plot(x, y, '-s', markevery=10, label='mark every 10') - ax.plot(x, y, '-+', markevery=(5, 20), label='mark every 5 starting at 10') + ax.plot(x, y+1, '-d', markevery=None, label='mark all') + ax.plot(x, y+2, '-s', markevery=10, label='mark every 10') + ax.plot(x, y+3, '-+', markevery=(5, 20), label='mark every 20 starting at 5') ax.legend() @@ -2013,7 +2000,8 @@ def test_marker_edges(): ax.plot(x+0.2, np.sin(x), 'y.', ms=30.0, mew=2, mec='b') -@image_comparison(['bar_tick_label_single.png', 'bar_tick_label_single.png']) +@image_comparison(['bar_tick_label_single.png', 'bar_tick_label_single.png'], + style='mpl20') def test_bar_tick_label_single(): # From 2516: plot bar with array of string labels for x axis ax = plt.gca() @@ -2036,7 +2024,7 @@ def test_bar_ticklabel_fail(): ax.bar([], []) -@image_comparison(['bar_tick_label_multiple.png']) +@image_comparison(['bar_tick_label_multiple.png'], style='mpl20') def test_bar_tick_label_multiple(): # From 2516: plot bar with array of string labels for x axis ax = plt.gca() @@ -2044,7 +2032,7 @@ def test_bar_tick_label_multiple(): align='center') -@image_comparison(['bar_tick_label_multiple_old_label_alignment.png']) +@image_comparison(['bar_tick_label_multiple_old_label_alignment.png'], style='mpl20') def test_bar_tick_label_multiple_old_alignment(): # Test that the alignment for class is backward compatible matplotlib.rcParams["ytick.alignment"] = "center" @@ -2125,7 +2113,7 @@ def test_bar_edgecolor_none_alpha(): assert rect.get_edgecolor() == (0, 0, 0, 0) -@image_comparison(['barh_tick_label.png']) +@image_comparison(['barh_tick_label.png'], style='mpl20') def test_barh_tick_label(): # From 2516: plot barh with array of string labels for y axis ax = plt.gca() @@ -2524,7 +2512,7 @@ def test_hist_step_filled(): assert all(p.get_facecolor() == p.get_edgecolor() for p in patches) -@image_comparison(['hist_density.png']) +@image_comparison(['hist_density.png'], style='mpl20') def test_hist_density(): np.random.seed(19680801) data = np.random.standard_normal(2000) @@ -2758,7 +2746,7 @@ def test_stairs_invalid_update2(): h.set_data(edges=np.arange(5)) -@image_comparison(['test_stairs_options.png'], remove_text=True) +@image_comparison(['test_stairs_options.png'], style='mpl20', remove_text=True) def test_stairs_options(): x, y = np.array([1, 2, 3, 4, 5]), np.array([1, 2, 3, 4]).astype(float) yn = y.copy() @@ -2782,8 +2770,7 @@ def test_stairs_options(): ax.legend(loc=0) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['test_stairs_datetime.png'], tol=0.2) +@image_comparison(['test_stairs_datetime.png'], style='mpl20') def test_stairs_datetime(): f, ax = plt.subplots(constrained_layout=True) ax.stairs(np.arange(36), @@ -3407,7 +3394,7 @@ def test_log_scales_invalid(): @image_comparison(['stackplot_test_image.png', 'stackplot_test_image.png'], - tol=0 if platform.machine() == 'x86_64' else 0.031) + style='mpl20') def test_stackplot(): fig = plt.figure() x = np.linspace(0, 10, 10) @@ -3568,10 +3555,7 @@ def test_bxp_horizontal(): _bxp_test_helper(bxp_kwargs=dict(orientation='horizontal')) -@image_comparison(['bxp_with_ylabels.png'], - savefig_kwarg={'dpi': 40}, - style='default', - tol=0.1) +@image_comparison(['bxp_with_ylabels.png'], savefig_kwarg={'dpi': 40}, style='default') def test_bxp_with_ylabels(): def transform(stats): for s, label in zip(stats, list('ABCD')): @@ -3772,7 +3756,7 @@ def test_bxp_bad_capwidths(): _bxp_test_helper(bxp_kwargs=dict(capwidths=[1])) -@image_comparison(['boxplot.png', 'boxplot.png'], tol=1.28, style='default') +@image_comparison(['boxplot.png', 'boxplot.png'], tol=0.43, style='default') def test_boxplot(): # Randomness used for bootstrapping. np.random.seed(937) @@ -4023,7 +4007,7 @@ def test_boxplot_mod_artist_after_plotting(): @image_comparison(['violinplot_vert_baseline.png', - 'violinplot_vert_baseline.png']) + 'violinplot_vert_baseline.png'], style='mpl20') def test_vert_violinplot_baseline(): # First 9 digits of frac(sqrt(2)) np.random.seed(414213562) @@ -4039,7 +4023,7 @@ def test_vert_violinplot_baseline(): showmedians=False, data=data) -@image_comparison(['violinplot_vert_showmeans.png']) +@image_comparison(['violinplot_vert_showmeans.png'], style='mpl20') def test_vert_violinplot_showmeans(): ax = plt.axes() # First 9 digits of frac(sqrt(3)) @@ -4049,7 +4033,7 @@ def test_vert_violinplot_showmeans(): showmedians=False) -@image_comparison(['violinplot_vert_showextrema.png']) +@image_comparison(['violinplot_vert_showextrema.png'], style='mpl20') def test_vert_violinplot_showextrema(): ax = plt.axes() # First 9 digits of frac(sqrt(5)) @@ -4059,7 +4043,7 @@ def test_vert_violinplot_showextrema(): showmedians=False) -@image_comparison(['violinplot_vert_showmedians.png']) +@image_comparison(['violinplot_vert_showmedians.png'], style='mpl20') def test_vert_violinplot_showmedians(): ax = plt.axes() # First 9 digits of frac(sqrt(7)) @@ -4069,7 +4053,7 @@ def test_vert_violinplot_showmedians(): showmedians=True) -@image_comparison(['violinplot_vert_showall.png']) +@image_comparison(['violinplot_vert_showall.png'], style='mpl20') def test_vert_violinplot_showall(): ax = plt.axes() # First 9 digits of frac(sqrt(11)) @@ -4080,7 +4064,7 @@ def test_vert_violinplot_showall(): quantiles=[[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.4, 0.6]]) -@image_comparison(['violinplot_vert_custompoints_10.png']) +@image_comparison(['violinplot_vert_custompoints_10.png'], style='mpl20') def test_vert_violinplot_custompoints_10(): ax = plt.axes() # First 9 digits of frac(sqrt(13)) @@ -4090,7 +4074,7 @@ def test_vert_violinplot_custompoints_10(): showmedians=False, points=10) -@image_comparison(['violinplot_vert_custompoints_200.png']) +@image_comparison(['violinplot_vert_custompoints_200.png'], style='mpl20') def test_vert_violinplot_custompoints_200(): ax = plt.axes() # First 9 digits of frac(sqrt(17)) @@ -4100,7 +4084,7 @@ def test_vert_violinplot_custompoints_200(): showmedians=False, points=200) -@image_comparison(['violinplot_horiz_baseline.png']) +@image_comparison(['violinplot_horiz_baseline.png'], style='mpl20') def test_horiz_violinplot_baseline(): ax = plt.axes() # First 9 digits of frac(sqrt(19)) @@ -4110,7 +4094,7 @@ def test_horiz_violinplot_baseline(): showextrema=False, showmedians=False) -@image_comparison(['violinplot_horiz_showmedians.png']) +@image_comparison(['violinplot_horiz_showmedians.png'], style='mpl20') def test_horiz_violinplot_showmedians(): ax = plt.axes() # First 9 digits of frac(sqrt(23)) @@ -4120,7 +4104,7 @@ def test_horiz_violinplot_showmedians(): showextrema=False, showmedians=True) -@image_comparison(['violinplot_horiz_showmeans.png']) +@image_comparison(['violinplot_horiz_showmeans.png'], style='mpl20') def test_horiz_violinplot_showmeans(): ax = plt.axes() # First 9 digits of frac(sqrt(29)) @@ -4130,7 +4114,7 @@ def test_horiz_violinplot_showmeans(): showextrema=False, showmedians=False) -@image_comparison(['violinplot_horiz_showextrema.png']) +@image_comparison(['violinplot_horiz_showextrema.png'], style='mpl20') def test_horiz_violinplot_showextrema(): ax = plt.axes() # First 9 digits of frac(sqrt(31)) @@ -4140,7 +4124,7 @@ def test_horiz_violinplot_showextrema(): showextrema=True, showmedians=False) -@image_comparison(['violinplot_horiz_showall.png']) +@image_comparison(['violinplot_horiz_showall.png'], style='mpl20') def test_horiz_violinplot_showall(): ax = plt.axes() # First 9 digits of frac(sqrt(37)) @@ -4151,7 +4135,7 @@ def test_horiz_violinplot_showall(): quantiles=[[0.1, 0.9], [0.2, 0.8], [0.3, 0.7], [0.4, 0.6]]) -@image_comparison(['violinplot_horiz_custompoints_10.png']) +@image_comparison(['violinplot_horiz_custompoints_10.png'], style='mpl20') def test_horiz_violinplot_custompoints_10(): ax = plt.axes() # First 9 digits of frac(sqrt(41)) @@ -4161,7 +4145,7 @@ def test_horiz_violinplot_custompoints_10(): showextrema=False, showmedians=False, points=10) -@image_comparison(['violinplot_horiz_custompoints_200.png']) +@image_comparison(['violinplot_horiz_custompoints_200.png'], style='mpl20') def test_horiz_violinplot_custompoints_200(): ax = plt.axes() # First 9 digits of frac(sqrt(43)) @@ -4470,7 +4454,8 @@ def test_tick_space_size_0(): plt.savefig(b, dpi=80, format='raw') -@image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png']) +@image_comparison(['errorbar_basic.png', 'errorbar_mixed.png', 'errorbar_basic.png'], + style='mpl20') def test_errorbar(): # longdouble due to floating point rounding issues with certain # computer chipsets @@ -4607,7 +4592,7 @@ def test_errorbar_shape(): ax.errorbar(x, y, yerr=yerr, xerr=xerr, fmt='o') -@image_comparison(['errorbar_limits.png']) +@image_comparison(['errorbar_limits.png'], style='mpl20') def test_errorbar_limits(): x = np.arange(0.5, 5.5, 0.5) y = np.exp(-x) @@ -4633,7 +4618,7 @@ def test_errorbar_limits(): color='red') # including upper and lower limits - ax.errorbar(x, y+1.5, marker='o', ms=8, xerr=xerr, yerr=yerr, + ax.errorbar(x, y+1.5, marker='o', ms=6, xerr=xerr, yerr=yerr, lolims=lolims, uplims=uplims, ls=ls, color='magenta') # including xlower and xupper limits @@ -4646,7 +4631,7 @@ def test_errorbar_limits(): uplims = np.zeros_like(x) lolims[[6]] = True uplims[[3]] = True - ax.errorbar(x, y+2.1, marker='o', ms=8, xerr=xerr, yerr=yerr, + ax.errorbar(x, y+2.1, marker='o', ms=6, xerr=xerr, yerr=yerr, xlolims=xlolims, xuplims=xuplims, uplims=uplims, lolims=lolims, ls='none', mec='blue', capsize=0, color='cyan') @@ -4857,7 +4842,8 @@ def test_errorbar_masked_negative(fig_test, fig_ref): ax.errorbar([4], [3], yerr=[6], fmt="C0") -@image_comparison(['hist_stacked_stepfilled.png', 'hist_stacked_stepfilled.png']) +@image_comparison(['hist_stacked_stepfilled.png', 'hist_stacked_stepfilled.png'], + style='mpl20') def test_hist_stacked_stepfilled(): # make some data d1 = np.linspace(1, 3, 20) @@ -4871,7 +4857,7 @@ def test_hist_stacked_stepfilled(): ax.hist("x", histtype="stepfilled", stacked=True, data=data) -@image_comparison(['hist_offset.png']) +@image_comparison(['hist_offset.png'], style='mpl20') def test_hist_offset(): # make some data d1 = np.linspace(0, 10, 50) @@ -4891,7 +4877,7 @@ def test_hist_step(): ax.set_xlim(-1, 5) -@image_comparison(['hist_step_horiz.png']) +@image_comparison(['hist_step_horiz.png'], style='mpl20') def test_hist_step_horiz(): # make some data d1 = np.linspace(0, 10, 50) @@ -4900,7 +4886,7 @@ def test_hist_step_horiz(): ax.hist((d1, d2), histtype="step", orientation="horizontal") -@image_comparison(['hist_stacked_weights.png']) +@image_comparison(['hist_stacked_weights.png'], style='mpl20') def test_hist_stacked_weighted(): # make some data d1 = np.linspace(0, 10, 50) @@ -5042,7 +5028,7 @@ def test_stem_polar_baseline(): assert container.baseline.get_path()._interpolation_steps > 100 -@image_comparison(['hist_stacked_stepfilled_alpha.png']) +@image_comparison(['hist_stacked_stepfilled_alpha.png'], style='mpl20') def test_hist_stacked_stepfilled_alpha(): # make some data d1 = np.linspace(1, 3, 20) @@ -5051,7 +5037,7 @@ def test_hist_stacked_stepfilled_alpha(): ax.hist((d1, d2), histtype="stepfilled", stacked=True, alpha=0.5) -@image_comparison(['hist_stacked_step.png']) +@image_comparison(['hist_stacked_step.png'], style='mpl20') def test_hist_stacked_step(): # make some data d1 = np.linspace(1, 3, 20) @@ -5060,7 +5046,7 @@ def test_hist_stacked_step(): ax.hist((d1, d2), histtype="step", stacked=True) -@image_comparison(['hist_stacked_normed.png']) +@image_comparison(['hist_stacked_normed.png'], style='mpl20') def test_hist_stacked_density(): # make some data d1 = np.linspace(1, 3, 20) @@ -5148,7 +5134,7 @@ def test_hist_stacked_step_bottom_geometry(): assert_array_equal(polygon.get_xy(), xy[1]) -@image_comparison(['hist_stacked_bar.png']) +@image_comparison(['hist_stacked_bar.png'], style='mpl20') def test_hist_stacked_bar(): # make some data d = [[100, 100, 100, 100, 200, 320, 450, 80, 20, 600, 310, 800], @@ -5159,7 +5145,7 @@ def test_hist_stacked_bar(): colors = [(0.5759849696758961, 1.0, 0.0), (0.0, 1.0, 0.350624650815206), (0.0, 1.0, 0.6549834156005998), (0.0, 0.6569064625276622, 1.0), (0.28302699607823545, 0.0, 1.0), (0.6849123462299822, 0.0, 1.0)] - labels = ['green', 'orange', ' yellow', 'magenta', 'black'] + labels = ['first', 'second', 'third', 'fourth', 'fifth'] fig, ax = plt.subplots() ax.hist(d, bins=10, histtype='barstacked', align='mid', color=colors, label=labels) @@ -5551,8 +5537,8 @@ def test_marker_styles(): marker=marker, markersize=10+y/5, label=marker) -@image_comparison(['rc_markerfill.png'], - tol=0 if platform.machine() == 'x86_64' else 0.037) +@image_comparison(['rc_markerfill.png'], style='mpl20', + tol=0.033 if sys.platform == 'darwin' else 0) def test_markers_fillstyle_rcparams(): fig, ax = plt.subplots() x = np.arange(7) @@ -5574,8 +5560,8 @@ def test_vertex_markers(): ax.set_ylim(-1, 10) -@image_comparison(['vline_hline_zorder.png', 'errorbar_zorder.png'], - tol=0 if platform.machine() == 'x86_64' else 0.026) +@image_comparison(['vline_hline_zorder.png', 'errorbar_zorder.png'], style='mpl20', + tol=0.02 if sys.platform == 'darwin' else 0) def test_eb_line_zorder(): x = list(range(10)) @@ -5697,7 +5683,8 @@ def test_axline_args(): plt.draw() -@image_comparison(['vlines_basic.png', 'vlines_with_nan.png', 'vlines_masked.png']) +@image_comparison(['vlines_basic.png', 'vlines_with_nan.png', 'vlines_masked.png'], + style='mpl20') def test_vlines(): # normal x1 = [2, 3, 4, 5, 7] @@ -5743,7 +5730,8 @@ def test_vlines_default(): assert mpl.colors.same_color(lines.get_color(), 'red') -@image_comparison(['hlines_basic.png', 'hlines_with_nan.png', 'hlines_masked.png']) +@image_comparison(['hlines_basic.png', 'hlines_with_nan.png', 'hlines_masked.png'], + style='mpl20') def test_hlines(): # normal y1 = [2, 3, 4, 5, 7] @@ -6486,12 +6474,7 @@ def test_text_labelsize(): ax.tick_params(direction='out') -# Note: The `pie` image tests were affected by Numpy 2.0 changing promotions -# (NEP 50). While the changes were only marginal, tolerances were introduced. -# These tolerances could likely go away when numpy 2.0 is the minimum supported -# numpy and the images are regenerated. - -@image_comparison(['pie_default.png'], tol=0.01) +@image_comparison(['pie_default.png'], style='mpl20') def test_pie_default(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6504,7 +6487,7 @@ def test_pie_default(): @image_comparison(['pie_linewidth_0.png', 'pie_linewidth_0.png', 'pie_linewidth_0.png'], - style='mpl20', tol=0.01) + style='mpl20') def test_pie_linewidth_0(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6536,7 +6519,8 @@ def test_pie_linewidth_0(): plt.axis('equal') -@image_comparison(['pie_center_radius.png'], style='mpl20', tol=0.01) +@image_comparison(['pie_center_radius.png'], style='mpl20', + tol=0.01 if sys.platform == 'darwin' else 0) def test_pie_center_radius(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6556,7 +6540,7 @@ def test_pie_center_radius(): plt.axis('equal') -@image_comparison(['pie_linewidth_2.png'], style='mpl20', tol=0.01) +@image_comparison(['pie_linewidth_2.png'], style='mpl20') def test_pie_linewidth_2(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6571,7 +6555,7 @@ def test_pie_linewidth_2(): plt.axis('equal') -@image_comparison(['pie_ccw_true.png'], style='mpl20', tol=0.01) +@image_comparison(['pie_ccw_true.png'], style='mpl20') def test_pie_ccw_true(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6586,7 +6570,7 @@ def test_pie_ccw_true(): plt.axis('equal') -@image_comparison(['pie_frame_grid.png'], style='mpl20', tol=0.002) +@image_comparison(['pie_frame_grid.png'], style='mpl20') def test_pie_frame_grid(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -6613,8 +6597,7 @@ def test_pie_frame_grid(): plt.axis('equal') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['pie_rotatelabels_true.png'], style='mpl20', tol=0.1) +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20') def test_pie_rotatelabels_true(): # The slices will be ordered and plotted counter-clockwise. labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' @@ -6629,7 +6612,7 @@ def test_pie_rotatelabels_true(): plt.axis('equal') -@image_comparison(['pie_no_label.png'], tol=0.01) +@image_comparison(['pie_no_label.png'], style='mpl20') def test_pie_nolabel_but_legend(): labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' sizes = [15, 30, 45, 10] @@ -6795,8 +6778,8 @@ def test_pie_label_fail(): ax.pie_label(pie, labels) -@image_comparison(['set_get_ticklabels.png'], - tol=0 if platform.machine() == 'x86_64' else 0.025) +@image_comparison(['set_get_ticklabels.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.03) def test_set_get_ticklabels(): # test issue 2246 fig, ax = plt.subplots(2) @@ -6897,7 +6880,7 @@ def test_empty_ticks_fixed_loc(): ax.set_xticklabels([]) -@image_comparison(['retain_tick_visibility.png']) +@image_comparison(['retain_tick_visibility.png'], style='mpl20') def test_retain_tick_visibility(): fig, ax = plt.subplots() plt.plot([0, 1, 2], [0, -1, 4]) @@ -6939,7 +6922,7 @@ def formatter_func(x, pos): assert tick_texts == ["", "", "unit value", "", ""] -@image_comparison(['o_marker_path_snap.png'], savefig_kwarg={'dpi': 72}) +@image_comparison(['o_marker_path_snap.png'], savefig_kwarg={'dpi': 72}, style='mpl20') def test_o_marker_path_snap(): fig, ax = plt.subplots() ax.margins(.1) @@ -7117,7 +7100,7 @@ def test_move_offsetlabel(): assert ax.xaxis.offsetText.get_verticalalignment() == 'bottom' -@image_comparison(['rc_spines.png'], savefig_kwarg={'dpi': 40}) +@image_comparison(['rc_spines.png'], savefig_kwarg={'dpi': 40}, style='mpl20') def test_rc_spines(): rc_dict = { 'axes.spines.left': False, @@ -7128,7 +7111,7 @@ def test_rc_spines(): plt.subplots() # create a figure and axes with the spine properties -@image_comparison(['rc_grid.png'], savefig_kwarg={'dpi': 40}) +@image_comparison(['rc_grid.png'], savefig_kwarg={'dpi': 40}, style='mpl20') def test_rc_grid(): fig = plt.figure() rc_dict0 = { @@ -7753,7 +7736,7 @@ def test_titletwiny(): bbox_y0_title = title.get_window_extent(renderer).y0 # bottom of title bbox_y1_xlabel2 = xlabel2.get_window_extent(renderer).y1 # top of xlabel2 y_diff = bbox_y0_title - bbox_y1_xlabel2 - assert np.isclose(y_diff, 3) + assert y_diff >= 3 def test_titlesetpos(): @@ -8355,7 +8338,7 @@ def inverted(self): @image_comparison(['secondary_xy.png'], style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.027) + tol=0 if platform.machine() == 'x86_64' else 0.024) def test_secondary_xy(): fig, axs = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True) @@ -8529,8 +8512,8 @@ def test_normal_axes(): # test the axis bboxes target = [ - [123.375, 75.88888888888886, 983.25, 33.0], - [85.51388888888889, 99.99999999999997, 53.375, 993.0] + [124.0, 75.56, 982.0, 33.33], + [86.89, 99.33, 52.0, 993.33], ] for nn, b in enumerate(bbaxis): targetbb = mtransforms.Bbox.from_bounds(*target[nn]) @@ -8550,7 +8533,7 @@ def test_normal_axes(): targetbb = mtransforms.Bbox.from_bounds(*target) assert_array_almost_equal(bbax.bounds, targetbb.bounds, decimal=2) - target = [85.5138, 75.88888, 1021.11, 1017.11] + target = [86.89, 75.56, 1019.11, 1017.11] targetbb = mtransforms.Bbox.from_bounds(*target) assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=1) @@ -9166,7 +9149,7 @@ def test_bar_label_location_center(): assert labels[1].get_verticalalignment() == 'center' -@image_comparison(['test_centered_bar_label_nonlinear.svg']) +@image_comparison(['test_centered_bar_label_nonlinear.svg'], style='mpl20') def test_centered_bar_label_nonlinear(): _, ax = plt.subplots() bar_container = ax.barh(['c', 'b', 'a'], [1_000, 5_000, 7_000]) @@ -9647,7 +9630,7 @@ def test_zorder_and_explicit_rasterization(): @image_comparison(["preset_clip_paths.png"], remove_text=True, style="mpl20", - tol=0 if platform.machine() == 'x86_64' else 0.027) + tol=0.01 if sys.platform == 'darwin' else 0) def test_preset_clip_paths(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 0bbb06ab37b1..20776af13307 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -2,6 +2,7 @@ import decimal import io from pathlib import Path +import string import numpy as np import pytest @@ -345,7 +346,7 @@ def test_empty_rasterized(): fig.savefig(io.BytesIO(), format="pdf") -@image_comparison(['kerning.pdf']) +@image_comparison(['kerning.pdf'], style='mpl20') def test_kerning(): fig = plt.figure() s = "AVAVAVAVAVAVAVAV€AAVV" @@ -360,13 +361,13 @@ def test_glyphs_subset(): # non-subsetted FT2Font nosubfont = FT2Font(fpath) nosubfont.set_text(chars) + nosubcmap = nosubfont.get_charmap() # subsetted FT2Font - with get_glyphs_subset(fpath, chars) as subset: + glyph_indices = {nosubcmap[ord(c)] for c in chars} + with get_glyphs_subset(fm.FontPath(fpath, 0), glyph_indices) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) - - nosubcmap = nosubfont.get_charmap() subcmap = subfont.get_charmap() # all unique chars must be available in subsetted font @@ -379,28 +380,60 @@ def test_glyphs_subset(): assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs() -@image_comparison(["multi_font_type3.pdf"]) +@image_comparison(["multi_font_type3.pdf"], style='mpl20') def test_multi_font_type3(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('pdf', fonttype=3) - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') -@image_comparison(["multi_font_type42.pdf"]) +@image_comparison(["multi_font_type42.pdf"], style='mpl20') def test_multi_font_type42(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('pdf', fonttype=42) - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.pdf'], style='mpl20') +def test_ttc_type3(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.pdf'], style='mpl20') +def test_ttc_type42(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @pytest.mark.parametrize('family_name, file_name', [("Noto Sans", "NotoSans-Regular.otf"), ("FreeMono", "FreeMono.otf")]) @@ -417,14 +450,14 @@ def test_otf_font_smoke(family_name, file_name): fig.savefig(io.BytesIO(), format="pdf") -@image_comparison(["truetype-conversion.pdf"]) +@image_comparison(["truetype-conversion.pdf"], style='mpl20') # mpltest.ttf does not have "l"/"p" glyphs so we get a warning when trying to # get the font extents. def test_truetype_conversion(recwarn): mpl.rcParams['pdf.fonttype'] = 3 fig, ax = plt.subplots() ax.text(0, 0, "ABCDE", - font=Path(__file__).parent / "data/mpltest.ttf", fontsize=80) + font=Path(__file__).parent / "data/mpltest.ttf", fontsize=72) ax.set_xticks([]) ax.set_yticks([]) diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e218a81cdceb..e5b73c9450f3 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -1,7 +1,9 @@ import datetime from io import BytesIO import os +from pathlib import Path import shutil +import string import numpy as np from packaging.version import parse as parse_version @@ -9,6 +11,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties, findfont from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images @@ -330,6 +333,23 @@ def test_png_transparency(): # Actually, also just testing that png works. assert (t[..., 3] == 0).all() # fully transparent. +@needs_pgf_xelatex +@pytest.mark.backend('pgf') +@image_comparison(['ttc_pgf.pdf'], style='mpl20') +def test_ttc_output(): + fp = FontProperties(family=['WenQuanYi Zen Hei']) + if Path(findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = {'sans-serif': 'WenQuanYi Zen Hei', 'monospace': 'WenQuanYi Zen Hei Mono'} + plt.rc('font', size=16, **fonts) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts.values(), figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @needs_pgf_xelatex def test_unknown_font(caplog): with caplog.at_level("WARNING"): diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 9859a286e5fd..6eac82678362 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -1,12 +1,14 @@ from collections import Counter import io +from pathlib import Path import re +import string import tempfile import numpy as np import pytest -from matplotlib import cbook, path, patheffects +from matplotlib import cbook, font_manager, path, patheffects from matplotlib.figure import Figure from matplotlib.patches import Ellipse from matplotlib.testing import _gen_multi_font_text @@ -217,11 +219,6 @@ def test_useafm(): ax.text(.5, .5, "qk") -@image_comparison(["type3.eps"]) -def test_type3_font(): - plt.figtext(.5, .5, "I/J") - - @image_comparison(["coloredhatcheszerolw.eps"]) def test_colored_hatch_zero_linewidth(): ax = plt.gca() @@ -318,28 +315,60 @@ def test_no_duplicate_definition(): assert max(Counter(wds).values()) == 1 -@image_comparison(["multi_font_type3.eps"]) +@image_comparison(["multi_font_type3.eps"], style='mpl20') def test_multi_font_type3(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('ps', fonttype=3) - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') -@image_comparison(["multi_font_type42.eps"]) +@image_comparison(["multi_font_type42.eps"], style='mpl20') def test_multi_font_type42(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('ps', fonttype=42) - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.eps'], style='mpl20') +def test_ttc_type3(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.eps'], style='mpl20') +def test_ttc_type42(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @image_comparison(["scatter.eps"]) def test_path_collection(): rng = np.random.default_rng(19680801) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 7864b3bb68bd..6c540ccebd76 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -68,7 +68,7 @@ def test_text_urls(): assert expected in buf -@image_comparison(['bold_font_output.svg']) +@image_comparison(['bold_font_output.svg'], style='mpl20') def test_bold_font_output(): fig, ax = plt.subplots() ax.plot(np.arange(10), np.arange(10)) @@ -218,7 +218,7 @@ def test_unicode_won(): tree = xml.etree.ElementTree.fromstring(buf) ns = 'http://www.w3.org/2000/svg' - won_id = 'SFSS1728-8e' + won_id = 'SFSS1728-232' assert len(tree.findall(f'.//{{{ns}}}path[@d][@id="{won_id}"]')) == 1 assert f'#{won_id}' in tree.find(f'.//{{{ns}}}use').attrib.values() @@ -527,24 +527,24 @@ def test_svg_metadata(): assert values == metadata['Keywords'] -@image_comparison(["multi_font_aspath.svg"]) -def test_multi_font_type3(): +@image_comparison(["multi_font_aspath.svg"], style='mpl20') +def test_multi_font_aspath(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('svg', fonttype='path') - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') -@image_comparison(["multi_font_astext.svg"]) -def test_multi_font_type42(): +@image_comparison(["multi_font_astext.svg"], style='mpl20') +def test_multi_font_astext(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('svg', fonttype='none') - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) fig.text(0.5, 0.5, test_str, horizontalalignment='center', verticalalignment='center') diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 0bda0b6fbec3..f6d910a7f208 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -46,8 +46,8 @@ def test_bbox_inches_tight(text_placeholders): @image_comparison(['bbox_inches_tight_suptile_legend'], - savefig_kwarg={'bbox_inches': 'tight'}, - tol=0 if platform.machine() == 'x86_64' else 0.02) + savefig_kwarg={'bbox_inches': 'tight'}, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.024) def test_bbox_inches_tight_suptile_legend(): plt.plot(np.arange(10), label='a straight line') plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left') @@ -66,7 +66,7 @@ def y_formatter(y, pos): @image_comparison(['bbox_inches_tight_suptile_non_default.png'], - savefig_kwarg={'bbox_inches': 'tight'}, + savefig_kwarg={'bbox_inches': 'tight'}, style='mpl20', tol=0.1) # large tolerance because only testing clipping. def test_bbox_inches_tight_suptitle_non_default(): fig, ax = plt.subplots() @@ -111,7 +111,8 @@ def test_bbox_inches_tight_clipping(): @image_comparison(['bbox_inches_tight_raster'], tol=0.15, # For Ghostscript 10.06+. - remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}) + remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}, + style='mpl20') def test_bbox_inches_tight_raster(): """Test rasterization with tight_layout""" fig, ax = plt.subplots() @@ -168,7 +169,7 @@ def test_noop_tight_bbox(): @image_comparison(['bbox_inches_fixed_aspect.png'], remove_text=True, - savefig_kwarg={'bbox_inches': 'tight'}) + savefig_kwarg={'bbox_inches': 'tight'}, style='mpl20') def test_bbox_inches_fixed_aspect(): with plt.rc_context({'figure.constrained_layout.use': True}): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index c0ac4ac28c8b..dc397ffde93e 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -66,7 +66,7 @@ def generate_EventCollection_plot(): return ax, coll, props -@image_comparison(['EventCollection_plot__default.png']) +@image_comparison(['EventCollection_plot__default.png'], style='mpl20') def test__EventCollection__get_props(): _, coll, props = generate_EventCollection_plot() # check that the default segments have the correct coordinates @@ -92,7 +92,7 @@ def test__EventCollection__get_props(): np.testing.assert_array_equal(color, props['color']) -@image_comparison(['EventCollection_plot__set_positions.png']) +@image_comparison(['EventCollection_plot__set_positions.png'], style='mpl20') def test__EventCollection__set_positions(): splt, coll, props = generate_EventCollection_plot() new_positions = np.hstack([props['positions'], props['extra_positions']]) @@ -106,7 +106,7 @@ def test__EventCollection__set_positions(): splt.set_xlim(-1, 90) -@image_comparison(['EventCollection_plot__add_positions.png']) +@image_comparison(['EventCollection_plot__add_positions.png'], style='mpl20') def test__EventCollection__add_positions(): splt, coll, props = generate_EventCollection_plot() new_positions = np.hstack([props['positions'], @@ -124,7 +124,7 @@ def test__EventCollection__add_positions(): splt.set_xlim(-1, 35) -@image_comparison(['EventCollection_plot__append_positions.png']) +@image_comparison(['EventCollection_plot__append_positions.png'], style='mpl20') def test__EventCollection__append_positions(): splt, coll, props = generate_EventCollection_plot() new_positions = np.hstack([props['positions'], @@ -140,7 +140,7 @@ def test__EventCollection__append_positions(): splt.set_xlim(-1, 90) -@image_comparison(['EventCollection_plot__extend_positions.png']) +@image_comparison(['EventCollection_plot__extend_positions.png'], style='mpl20') def test__EventCollection__extend_positions(): splt, coll, props = generate_EventCollection_plot() new_positions = np.hstack([props['positions'], @@ -156,7 +156,7 @@ def test__EventCollection__extend_positions(): splt.set_xlim(-1, 90) -@image_comparison(['EventCollection_plot__switch_orientation.png']) +@image_comparison(['EventCollection_plot__switch_orientation.png'], style='mpl20') def test__EventCollection__switch_orientation(): splt, coll, props = generate_EventCollection_plot() new_orientation = 'vertical' @@ -173,7 +173,7 @@ def test__EventCollection__switch_orientation(): splt.set_xlim(0, 2) -@image_comparison(['EventCollection_plot__switch_orientation__2x.png']) +@image_comparison(['EventCollection_plot__switch_orientation__2x.png'], style='mpl20') def test__EventCollection__switch_orientation_2x(): """ Check that calling switch_orientation twice sets the orientation back to @@ -194,7 +194,7 @@ def test__EventCollection__switch_orientation_2x(): splt.set_title('EventCollection: switch_orientation 2x') -@image_comparison(['EventCollection_plot__set_orientation.png']) +@image_comparison(['EventCollection_plot__set_orientation.png'], style='mpl20') def test__EventCollection__set_orientation(): splt, coll, props = generate_EventCollection_plot() new_orientation = 'vertical' @@ -211,7 +211,7 @@ def test__EventCollection__set_orientation(): splt.set_xlim(0, 2) -@image_comparison(['EventCollection_plot__set_linelength.png']) +@image_comparison(['EventCollection_plot__set_linelength.png'], style='mpl20') def test__EventCollection__set_linelength(): splt, coll, props = generate_EventCollection_plot() new_linelength = 15 @@ -226,7 +226,7 @@ def test__EventCollection__set_linelength(): splt.set_ylim(-20, 20) -@image_comparison(['EventCollection_plot__set_lineoffset.png']) +@image_comparison(['EventCollection_plot__set_lineoffset.png'], style='mpl20') def test__EventCollection__set_lineoffset(): splt, coll, props = generate_EventCollection_plot() new_lineoffset = -5. @@ -245,11 +245,12 @@ def test__EventCollection__set_lineoffset(): 'EventCollection_plot__set_linestyle.png', 'EventCollection_plot__set_linestyle.png', 'EventCollection_plot__set_linewidth.png', -]) +], style='mpl20') def test__EventCollection__set_prop(): for prop, value, expected in [ - ('linestyle', 'dashed', [(0, (6.0, 6.0))]), - ('linestyle', (0, (6., 6.)), [(0, (6.0, 6.0))]), + ('linestyle', 'dashed', [(0, [7.4, 3.2])]), + # Dashes are scaled by linewidth. + ('linestyle', (0, (3.7, 1.6)), [(0, [7.4, 3.2])]), ('linewidth', 5, 5), ]: splt, coll, _ = generate_EventCollection_plot() @@ -258,7 +259,7 @@ def test__EventCollection__set_prop(): splt.set_title(f'EventCollection: set_{prop}') -@image_comparison(['EventCollection_plot__set_color.png']) +@image_comparison(['EventCollection_plot__set_color.png'], style='mpl20') def test__EventCollection__set_color(): splt, coll, _ = generate_EventCollection_plot() new_color = np.array([0, 1, 1, 1]) @@ -720,7 +721,7 @@ def test_joinstyle(): assert col.get_joinstyle() == 'miter' -@image_comparison(['cap_and_joinstyle.png']) +@image_comparison(['cap_and_joinstyle.png'], style='mpl20') def test_cap_and_joinstyle_image(): fig, ax = plt.subplots() ax.set_xlim([-0.5, 1.5]) diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 77ff797be11d..0991221f1339 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -152,18 +152,14 @@ def test_colorbar_extension_inverted_axis(orientation, extend, expected): assert len(cbar._extend_patches) == 1 -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @pytest.mark.parametrize('use_gridspec', [True, False]) @image_comparison(['cbar_with_orientation.png', 'cbar_locationing.png', 'double_cbar.png', 'cbar_sharing.png', ], - remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) + remove_text=True, savefig_kwarg={'dpi': 40}, style='mpl20') def test_colorbar_positioning(use_gridspec): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - data = np.arange(1200).reshape(30, 40) levels = [0, 200, 400, 600, 800, 1000, 1200] @@ -731,8 +727,7 @@ def test_colorbar_label(): assert cbar3.ax.get_xlabel() == 'horizontal cbar' -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20', tol=0.03) +@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20') def test_keeping_xlabel(): # github issue #23398 - xlabels being ignored in colorbar axis arr = np.arange(25).reshape((5, 5)) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 24a2f31f7594..808770e1d52c 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -828,11 +828,8 @@ def _mask_tester(norm_instance, vals): assert_array_equal(masked_array.mask, norm_instance(masked_array).mask) -@image_comparison(['levels_and_colors.png']) +@image_comparison(['levels_and_colors.png'], style='mpl20') def test_cmap_and_norm_from_levels_and_colors(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - data = np.linspace(-2, 4, 49).reshape(7, 7) levels = [-1, 2, 2.5, 3] colors = ['red', 'green', 'blue', 'yellow', 'black'] @@ -847,11 +844,8 @@ def test_cmap_and_norm_from_levels_and_colors(): ax.tick_params(labelleft=False, labelbottom=False) -@image_comparison(['boundarynorm_and_colorbar.png'], tol=1.0) +@image_comparison(['boundarynorm_and_colorbar.png']) def test_boundarynorm_and_colorbarbase(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - # Make a figure and axes with dimensions as desired. fig = plt.figure() ax1 = fig.add_axes((0.05, 0.80, 0.9, 0.15)) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 91aaa2fd9172..ff757c1ce9fc 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -411,9 +411,6 @@ def test_colorbar_location(): Test that colorbar handling is as expected for various complicated cases... """ - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(4, 5, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax) diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 42ad75862b2e..5c2674355d10 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -94,7 +94,7 @@ def test_contour_set_paths(fig_test, fig_ref): cs_test.set_paths(cs_ref.get_paths()) -@image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20', tol=0.26) +@image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20') def test_contour_manual_labels(): x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10)) z = np.max(np.dstack([abs(x), abs(y)]), 2) @@ -127,9 +127,8 @@ def test_contour_manual_moveto(): assert clabels[0].get_text() == "0" -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['contour_disconnected_segments.png'], - remove_text=True, style='mpl20', tol=0.01) + remove_text=True, style='mpl20') def test_contour_label_with_disconnected_segments(): x, y = np.mgrid[-1:1:21j, -1:1:21j] z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2) @@ -229,8 +228,7 @@ def test_lognorm_levels(n_levels): assert len(visible_levels) <= n_levels + 1 -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['contour_datetime_axis.png'], style='mpl20', tol=0.3) +@image_comparison(['contour_datetime_axis.png'], style='mpl20') def test_contour_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -256,7 +254,8 @@ def test_contour_datetime_axis(): @image_comparison(['contour_test_label_transforms.png'], - remove_text=True, style='mpl20', tol=1.1) + remove_text=True, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.005) def test_labels(): # Adapted from pylab_examples example code: contour_demo.py # see issues #2475, #2843, and #2818 for explanation diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index d3f64d73002e..45948ea1e7f0 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -140,7 +140,7 @@ def test_axhline(): mdates._reset_epoch_test_example() -@image_comparison(['date_axhspan.png']) +@image_comparison(['date_axhspan.png'], style='mpl20') def test_date_axhspan(): # test axhspan with date inputs t0 = datetime.datetime(2009, 1, 20) @@ -152,8 +152,7 @@ def test_date_axhspan(): fig.subplots_adjust(left=0.25) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['date_axvspan.png'], tol=0.07) +@image_comparison(['date_axvspan.png'], style='mpl20') def test_date_axvspan(): # test axvspan with date inputs t0 = datetime.datetime(2000, 1, 20) @@ -165,7 +164,7 @@ def test_date_axvspan(): fig.autofmt_xdate() -@image_comparison(['date_axhline.png']) +@image_comparison(['date_axhline.png'], style='mpl20') def test_date_axhline(): # test axhline with date inputs t0 = datetime.datetime(2009, 1, 20) @@ -177,8 +176,7 @@ def test_date_axhline(): fig.subplots_adjust(left=0.25) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['date_axvline.png'], tol=0.09) +@image_comparison(['date_axvline.png'], style='mpl20') def test_date_axvline(): # test axvline with date inputs t0 = datetime.datetime(2000, 1, 20) @@ -228,8 +226,7 @@ def wrapper(): return wrapper -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['RRuleLocator_bounds.png'], tol=0.07) +@image_comparison(['RRuleLocator_bounds.png'], style='mpl20') def test_RRuleLocator(): import matplotlib.testing.jpl_units as units units.register() @@ -273,8 +270,7 @@ def test_RRuleLocator_close_minmax(): assert list(map(str, mdates.num2date(loc.tick_values(d1, d2)))) == expected -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['DateFormatter_fractionalSeconds.png'], tol=0.11) +@image_comparison(['DateFormatter_fractionalSeconds.png'], style='mpl20') def test_DateFormatter(): import matplotlib.testing.jpl_units as units units.register() @@ -957,7 +953,7 @@ def _create_auto_date_locator(date1, date2, tz): assert st == expected -@image_comparison(['date_inverted_limit.png']) +@image_comparison(['date_inverted_limit.png'], style='mpl20') def test_date_inverted_limit(): # test ax hline with date inputs t0 = datetime.datetime(2009, 1, 20) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 0e793a6e7671..0e318cab0d4f 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -26,9 +26,8 @@ import matplotlib.dates as mdates -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['figure_align_labels'], extensions=['png', 'svg'], - tol=0.1 if platform.machine() == 'x86_64' else 0.1) +@image_comparison(['figure_align_labels'], extensions=['png', 'svg'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.01) def test_align_labels(): fig = plt.figure(layout='tight') gs = gridspec.GridSpec(3, 3) @@ -68,11 +67,9 @@ def test_align_labels(): fig.align_labels() -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['figure_align_titles_tight.png', 'figure_align_titles_constrained.png'], - tol=0.3 if platform.machine() == 'x86_64' else 0.04, - style='mpl20') + style='mpl20', tol=0 if platform.machine() == 'x86_64' else 0.021) def test_align_titles(): for layout in ['tight', 'constrained']: fig, axs = plt.subplots(1, 2, layout=layout, width_ratios=[2, 1]) @@ -210,8 +207,8 @@ def test_clf_keyword(): assert [t.get_text() for t in fig2.texts] == [] -@image_comparison(['figure_today.png'], - tol=0 if platform.machine() == 'x86_64' else 0.015) +@image_comparison(['figure_today.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.022) def test_figure(): # named figure support fig = plt.figure('today') @@ -226,7 +223,7 @@ def test_figure(): plt.close('tomorrow') -@image_comparison(['figure_legend.png']) +@image_comparison(['figure_legend.png'], style='mpl20') def test_figure_legend(): fig, axs = plt.subplots(2) axs[0].plot([0, 1], [1, 0], label='x', color='g') @@ -323,8 +320,7 @@ def test_add_subplot_invalid(): fig.add_subplot(ax) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['figure_suptitle.png'], tol=0.02) +@image_comparison(['figure_suptitle.png'], style='mpl20') def test_suptitle(): fig, _ = plt.subplots() fig.suptitle('hello', color='r') @@ -838,7 +834,7 @@ def test_tightbbox(): ax.set_xlim(0, 1) t = ax.text(1., 0.5, 'This dangles over end') renderer = fig.canvas.get_renderer() - x1Nom0 = 9.035 # inches + x1Nom0 = 8.9875 # inches assert abs(t.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(ax.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(fig.get_tightbbox(renderer).x1 - x1Nom0) < 0.05 @@ -1400,7 +1396,8 @@ def test_subfigure_dpi(): @image_comparison(['test_subfigure_ss.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, tol=0.02) + savefig_kwarg={'facecolor': 'teal'}, + tol=0.022 if sys.platform == 'darwin' else 0) def test_subfigure_ss(): # test assigning the subfigure via subplotspec np.random.seed(19680801) @@ -1422,9 +1419,8 @@ def test_subfigure_ss(): fig.suptitle('Figure suptitle', fontsize='xx-large') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['test_subfigure_double.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, tol=0.02) + savefig_kwarg={'facecolor': 'teal'}) def test_subfigure_double(): # test assigning the subfigure via subplotspec np.random.seed(19680801) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 1b6e7b4778a1..ab6c1d91b106 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -1,4 +1,4 @@ -from io import BytesIO, StringIO +from io import BytesIO import gc import multiprocessing import os @@ -11,11 +11,14 @@ import numpy as np import pytest +from unittest.mock import MagicMock, patch + import matplotlib as mpl +import matplotlib.font_manager as fm_mod from matplotlib.font_manager import ( - findfont, findSystemFonts, FontEntry, FontProperties, fontManager, + findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, - MSUserFontDirectories, ttfFontProperty, + MSUserFontDirectories, ttfFontProperty, _get_font_alt_names, _get_fontconfig_fonts, _normalize_weight) from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing @@ -24,6 +27,38 @@ has_fclist = sys.platform != 'emscripten' and shutil.which('fc-list') is not None +def test_font_path(): + fp = FontPath('foo', 123) + fp2 = FontPath('foo', 321) + assert str(fp) == 'foo' + assert repr(fp) == "FontPath('foo', 123)" + assert fp.path == 'foo' + assert fp.face_index == 123 + # Should be immutable. + with pytest.raises(AttributeError, match='has no setter'): + fp.path = 'bar' + with pytest.raises(AttributeError, match='has no setter'): + fp.face_index = 321 + # Should be comparable with str and itself. + assert fp == 'foo' + assert fp == FontPath('foo', 123) + assert fp <= fp + assert fp >= fp + assert fp != fp2 + assert fp < fp2 + assert fp <= fp2 + assert fp2 > fp + assert fp2 >= fp + # Should be hashable, but not the same as str. + d = {fp: 1, 'bar': 2} + assert fp in d + assert d[fp] == 1 + assert d[FontPath('foo', 123)] == 1 + assert fp2 not in d + assert 'foo' not in d + assert FontPath('bar', 0) not in d + + def test_font_priority(): with rc_context(rc={ 'font.sans-serif': @@ -67,12 +102,14 @@ def test_json_serialization(tmp_path): def test_otf(): fname = '/usr/share/fonts/opentype/freefont/FreeMono.otf' if Path(fname).exists(): - assert is_opentype_cff_font(fname) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + assert is_opentype_cff_font(fname) for f in fontManager.ttflist: if 'otf' in f.fname: with open(f.fname, 'rb') as fd: res = fd.read(4) == b'OTTO' - assert res == is_opentype_cff_font(f.fname) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + assert res == is_opentype_cff_font(f.fname) @pytest.mark.skipif(sys.platform == "win32" or not has_fclist, @@ -115,8 +152,17 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) - if Path(findfont(fp)).name != "wqy-zenhei.ttc": + fontpath = findfont(fp) + if Path(fontpath).name != "wqy-zenhei.ttc": pytest.skip("Font wqy-zenhei.ttc may be missing") + # All fonts from this collection should have loaded as well. + for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]: + subfontpath = findfont(FontProperties(family=[name]), fallback_to_default=False) + assert subfontpath.path == fontpath.path + assert subfontpath.face_index != fontpath.face_index + subfont = get_font(subfontpath) + assert subfont.fname == subfontpath.path + assert subfont.face_index == subfontpath.face_index fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: @@ -135,6 +181,34 @@ def test_find_noto(): fig.savefig(BytesIO(), format=fmt) +def test_find_valid(): + class PathLikeClass: + def __init__(self, filename): + self.filename = filename + + def __fspath__(self): + return self.filename + + file_str = findfont('DejaVu Sans') + file_bytes = os.fsencode(file_str) + + font = get_font(file_str) + assert font.fname == file_str + font = get_font(file_bytes) + assert font.fname == file_bytes + font = get_font(PathLikeClass(file_str)) + assert font.fname == file_str + font = get_font(PathLikeClass(file_bytes)) + assert font.fname == file_bytes + font = get_font(FontPath(file_str, 0)) + assert font.fname == file_str + + # Note, fallbacks are not currently accessible. + font = get_font([file_str, file_bytes, + PathLikeClass(file_str), PathLikeClass(file_bytes)]) + assert font.fname == file_str + + def test_find_invalid(tmp_path): with pytest.raises(FileNotFoundError): @@ -146,11 +220,6 @@ def test_find_invalid(tmp_path): with pytest.raises(FileNotFoundError): get_font(bytes(tmp_path / 'non-existent-font-name.ttf')) - # Not really public, but get_font doesn't expose non-filename constructor. - from matplotlib.ft2font import FT2Font - with pytest.raises(TypeError, match='font file or a binary-mode file'): - FT2Font(StringIO()) # type: ignore[arg-type] - @pytest.mark.skipif(sys.platform != 'linux' or not has_fclist, reason='only Linux with fontconfig installed') @@ -336,19 +405,145 @@ def test_get_font_names(): paths_mpl = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf']] fonts_mpl = findSystemFonts(paths_mpl, fontext='ttf') fonts_system = findSystemFonts(fontext='ttf') - ttf_fonts = [] + ttf_fonts = set() for path in fonts_mpl + fonts_system: try: font = ft2font.FT2Font(path) prop = ttfFontProperty(font) - ttf_fonts.append(prop.name) + ttf_fonts.add(prop.name) + for face_index in range(1, font.num_faces): + font = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(font) + ttf_fonts.add(prop.name) except Exception: pass - available_fonts = sorted(list(set(ttf_fonts))) - mpl_font_names = sorted(fontManager.get_font_names()) - assert set(available_fonts) == set(mpl_font_names) - assert len(available_fonts) == len(mpl_font_names) - assert available_fonts == mpl_font_names + # fontManager may contain additional entries for alternative family names + # (e.g. typographic family, platform-specific Name ID 1) registered by + # addfont(), so primary names must be a subset of the manager's names. + assert ttf_fonts <= set(fontManager.get_font_names()) + + +def test_addfont_alternative_names(tmp_path): + """ + Fonts that advertise different family names across platforms or name IDs + should be registered under all of those names so users can address the font + by any of them. + + Two real-world patterns are covered: + + - **MS platform ID 1 differs from Mac platform ID 1** (e.g. Ubuntu Light): + FreeType returns the Mac ID 1 value as ``family_name``; the MS ID 1 + value ("Ubuntu Light") is an equally valid name that users expect to work. + - **Name ID 16 (Typographic Family) differs from ID 1** (older fonts): + some fonts store a broader family name in ID 16. + """ + mac_key = (1, 0, 0) + ms_key = (3, 1, 0x0409) + + # Case 1: MS ID1 differs from Mac ID1 (Ubuntu Light pattern) + # Mac ID1="Test Family" → FreeType family_name (primary) + # MS ID1="Test Family Light" → alternate name users expect to work + ubuntu_style_sfnt = { + (*mac_key, 1): "Test Family".encode("latin-1"), + (*ms_key, 1): "Test Family Light".encode("utf-16-be"), + (*mac_key, 2): "Light".encode("latin-1"), + (*ms_key, 2): "Regular".encode("utf-16-be"), + } + fake_font = MagicMock() + fake_font.get_sfnt.return_value = ubuntu_style_sfnt + + assert _get_font_alt_names(fake_font, "Test Family") == [("Test Family Light", 400)] + assert _get_font_alt_names(fake_font, "Test Family Light") == [ + ("Test Family", 300)] + + # Case 2: ID 16 differs from ID 1 (older typographic-family pattern) + # ID 17 (typographic subfamily) is absent → defaults to weight 400 + id16_sfnt = { + (*mac_key, 1): "Test Family".encode("latin-1"), + (*ms_key, 1): "Test Family".encode("utf-16-be"), + (*ms_key, 16): "Test Family Light".encode("utf-16-be"), + } + fake_font_id16 = MagicMock() + fake_font_id16.get_sfnt.return_value = id16_sfnt + + assert _get_font_alt_names( + fake_font_id16, "Test Family" + ) == [("Test Family Light", 400)] + + # Case 3: all entries agree → no alternates + same_sfnt = { + (*mac_key, 1): "Test Family".encode("latin-1"), + (*ms_key, 1): "Test Family".encode("utf-16-be"), + } + fake_font_same = MagicMock() + fake_font_same.get_sfnt.return_value = same_sfnt + assert _get_font_alt_names(fake_font_same, "Test Family") == [] + + # Case 4: get_sfnt() raises ValueError (e.g. non-SFNT font) → empty list + fake_font_no_sfnt = MagicMock() + fake_font_no_sfnt.get_sfnt.side_effect = ValueError + assert _get_font_alt_names(fake_font_no_sfnt, "Test Family") == [] + + fake_path = str(tmp_path / "fake.ttf") + primary_entry = FontEntry(fname=fake_path, name="Test Family", + style="normal", variant="normal", + weight=300, stretch="normal", size="scalable") + + with patch("matplotlib.font_manager.ft2font.FT2Font", + return_value=fake_font), \ + patch("matplotlib.font_manager.ttfFontProperty", + return_value=primary_entry): + fm_instance = fm_mod.FontManager.__new__(fm_mod.FontManager) + fm_instance.ttflist = [] + fm_instance.afmlist = [] + fm_instance._findfont_cached = MagicMock() + fm_instance._findfont_cached.cache_clear = MagicMock() + fm_instance.addfont(fake_path) + + names = [e.name for e in fm_instance.ttflist] + assert names == ["Test Family", "Test Family Light"] + alt_entry = fm_instance.ttflist[1] + assert alt_entry.weight == 400 + assert alt_entry.style == primary_entry.style + assert alt_entry.fname == primary_entry.fname + + +@pytest.mark.parametrize("subfam,expected", [ + ("Thin", 100), + ("ExtraLight", 200), + ("UltraLight", 200), + ("DemiLight", 350), + ("SemiLight", 350), + ("Light", 300), + ("Book", 380), + ("Regular", 400), + ("Normal", 400), + ("Medium", 500), + ("DemiBold", 600), + ("Demi", 600), + ("SemiBold", 600), + ("ExtraBold", 800), + ("SuperBold", 800), + ("UltraBold", 800), + ("Bold", 700), + ("UltraBlack", 1000), + ("SuperBlack", 1000), + ("ExtraBlack", 1000), + ("Ultra", 1000), + ("Black", 900), + ("Heavy", 900), + ("", 400), # fallback: unrecognised → regular +]) +def test_alt_name_weight_from_subfamily(subfam, expected): + """_get_font_alt_names derives weight from the paired subfamily string.""" + ms_key = (3, 1, 0x0409) + fake_font = MagicMock() + fake_font.get_sfnt.return_value = { + (*ms_key, 1): "Family Alt".encode("utf-16-be"), + (*ms_key, 2): subfam.encode("utf-16-be"), + } + result = _get_font_alt_names(fake_font, "Family") + assert result == [("Family Alt", expected)] def test_donot_cache_tracebacks(): diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 8b448e17b7fd..8b44792a0c2d 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,6 +1,8 @@ import itertools import io +import os from pathlib import Path +from typing import cast import numpy as np import pytest @@ -133,6 +135,27 @@ def test_ft2font_stix_bold_attrs(): assert font.bbox == (4, -355, 1185, 2095) +def test_ft2font_valid_args(): + class PathLikeClass: + def __init__(self, filename): + self.filename = filename + + def __fspath__(self): + return self.filename + + file_str = fm.findfont('DejaVu Sans') + file_bytes = os.fsencode(file_str) + + font = ft2font.FT2Font(file_str) + assert font.fname == file_str + font = ft2font.FT2Font(file_bytes) + assert font.fname == file_bytes + font = ft2font.FT2Font(PathLikeClass(file_str)) + assert font.fname == file_str + font = ft2font.FT2Font(PathLikeClass(file_bytes)) + assert font.fname == file_bytes + + def test_ft2font_invalid_args(tmp_path): # filename argument. with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): @@ -168,6 +191,31 @@ def test_ft2font_invalid_args(tmp_path): # kerning_factor argument. with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _kerning_factor=1.3) + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='text.kerning_factor rcParam was deprecated .+ 3.11'): + mpl.rcParams['text.kerning_factor'] = 0 + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='_kerning_factor parameter was deprecated .+ 3.11'): + ft2font.FT2Font(file, _kerning_factor=123) + + +@pytest.mark.parametrize('name, size, skippable', + [('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)]) +def test_ft2font_face_index(name, size, skippable): + try: + file = fm.findfont(name, fallback_to_default=False) + except ValueError: + if skippable: + pytest.skip(r'Font {name} may be missing') + raise + for index in range(size): + font = ft2font.FT2Font(file, face_index=index) + assert font.num_faces >= size + assert font.face_index == index + with pytest.raises(ValueError, match='must be between'): # out of bounds for spec + ft2font.FT2Font(file, face_index=0x1ffff) + with pytest.raises(RuntimeError, match='invalid argument'): # invalid for this font + ft2font.FT2Font(file, face_index=0xff) def test_ft2font_clear(): @@ -188,8 +236,8 @@ def test_ft2font_clear(): 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 = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) font.set_text('ABabCDcd') orig = font.get_width_height() font.set_size(24, 72) @@ -200,6 +248,19 @@ def test_ft2font_set_size(): assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) +def test_ft2font_features(): + # Smoke test that these are accepted as intended. + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + font.set_text('foo', features=None) # unset + font.set_text('foo', features=['calt', 'dlig']) # list + font.set_text('foo', features=('calt', 'dlig')) # tuple + with pytest.raises(TypeError): + font.set_text('foo', features=123) + with pytest.raises(TypeError): + font.set_text('foo', features=[123, 456]) + + def test_ft2font_charmaps(): def enc(name): # We don't expose the encoding enum from FreeType, but can generate it here. @@ -235,7 +296,7 @@ def enc(name): assert unic == after # This is just a random sample from FontForge. - glyph_names = { + glyph_names = cast(dict[str, ft2font.GlyphIndexType], { 'non-existent-glyph-name': 0, 'plusminus': 115, 'Racute': 278, @@ -247,7 +308,7 @@ def enc(name): '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': @@ -526,9 +587,12 @@ def test_ft2font_get_sfnt(font_name, expected): 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, 'sFamilyClass': 0, 'panose': b'\x02\x0b\x06\x03\x03\x08\x04\x02\x02\x04', - 'ulCharRange': (3875565311, 3523280383, 170156073, 67117068), + 'ulUnicodeRange': (3875565311, 3523280383, 170156073, 67117068), 'achVendID': b'PfEd', - 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 65535, + 'fsSelection': 64, 'usFirstCharIndex': 32, 'usLastCharIndex': 65535, + 'sTypoAscender': 1556, 'sTypoDescender': -492, 'sTypoLineGap': 410, + 'usWinAscent': 1901, 'usWinDescent': 483, + 'ulCodePageRange': (1610613247, 3758030848), }, 'hhea': { 'version': (1, 0), @@ -592,9 +656,11 @@ def test_ft2font_get_sfnt(font_name, expected): 'yStrikeoutSize': 102, 'yStrikeoutPosition': 530, 'sFamilyClass': 0, 'panose': b'\x02\x0b\x05\x00\x00\x00\x00\x00\x00\x00', - 'ulCharRange': (0, 0, 0, 0), + 'ulUnicodeRange': (0, 0, 0, 0), 'achVendID': b'\x00\x00\x00\x00', - 'fsSelection': 64, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 9835, + 'fsSelection': 64, 'usFirstCharIndex': 32, 'usLastCharIndex': 9835, + 'sTypoAscender': 1276, 'sTypoDescender': -469, 'sTypoLineGap': 0, + 'usWinAscent': 1430, 'usWinDescent': 477, }, 'hhea': { 'version': (1, 0), @@ -672,9 +738,17 @@ def test_ft2font_get_sfnt(font_name, expected): 'yStrikeoutSize': 20, 'yStrikeoutPosition': 1037, 'sFamilyClass': 0, 'panose': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - 'ulCharRange': (3, 192, 0, 0), + 'ulUnicodeRange': (3, 192, 0, 0), 'achVendID': b'STIX', - 'fsSelection': 32, 'fsFirstCharIndex': 32, 'fsLastCharIndex': 10217, + 'fsSelection': 32, 'usFirstCharIndex': 32, 'usLastCharIndex': 10217, + 'sTypoAscender': 750, 'sTypoDescender': -250, 'sTypoLineGap': 1499, + 'usWinAscent': 2095, 'usWinDescent': 404, + 'ulCodePageRange': (2688417793, 2432565248), + 'sxHeight': 0, + 'sCapHeight': 0, + 'usDefaultChar': 0, + 'usBreakChar': 32, + 'usMaxContext': 1, }, 'hhea': { 'version': (1, 0), @@ -708,16 +782,16 @@ def test_ft2font_get_sfnt_table(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), + ('A', 'A', 57, 247, 256), ('A', 'À', 57, 247, 256), ('A', 'Á', 57, 247, 256), + ('A', 'Â', 57, 247, 256), ('A', 'Ã', 57, 247, 256), ('A', 'Ä', 57, 247, 256), # And a few other random ones. - ('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024), + ('D', 'A', -36, -156, -128), ('T', '.', -243, -1055, -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 = ft2font.FT2Font(file, hinting_factor=1) font.set_size(100, 100) assert font.get_kerning(font.get_char_index(ord(left)), font.get_char_index(ord(right)), @@ -756,7 +830,8 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): def test_ft2font_set_text(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) xys = font.set_text('') np.testing.assert_array_equal(xys, np.empty((0, 2))) assert font.get_width_height() == (0, 0) @@ -767,17 +842,49 @@ def test_ft2font_set_text(): 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) + [(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0), + (3065, 0), (3577, 0), (3940, 0)]) + assert font.get_width_height() == (4196, 768) assert font.get_num_glyphs() == 10 assert font.get_descent() == 192 assert font.get_bitmap_offset() == (6, 0) +@pytest.mark.parametrize( + 'input', + [ + [1, 2, 3], + [(1, 2)], + [('en', 'foo', 2)], + [('en', 1, 'foo')], + ], + ids=[ + 'nontuple', + 'wrong length', + 'wrong start type', + 'wrong end type', + ], +) +def test_ft2font_language_invalid(input): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + with pytest.raises(TypeError): + font.set_text('foo', language=input) + + +def test_ft2font_language(): + # This is just a smoke test. + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_text('foo') + font.set_text('foo', language='en') + font.set_text('foo', language=[('en', 1, 2)]) + + def test_ft2font_loading(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) for glyph in [font.load_char(ord('M')), font.load_glyph(font.get_char_index(ord('M')))]: assert glyph is not None @@ -817,12 +924,14 @@ def test_ft2font_drawing(): ]) expected *= 255 file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) 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) + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) glyph = font.load_char(ord('M')) image = np.zeros(expected.shape, np.uint8) font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) @@ -831,7 +940,8 @@ def test_ft2font_drawing(): def test_ft2font_get_path(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_size(12, 72) vertices, codes = font.get_path() assert vertices.shape == (0, 2) assert codes.shape == (0, ) @@ -883,7 +993,7 @@ def test_fallback_missing(recwarn, font_list): assert all([font in recwarn[0].message.args[0] for font in font_list]) -@image_comparison(['last_resort']) +@image_comparison(['last_resort'], style='mpl20') def test_fallback_last_resort(recwarn): fig = plt.figure(figsize=(3, 0.5)) fig.text(.5, .5, "Hello 🙃 World!", size=24, @@ -894,7 +1004,7 @@ def test_fallback_last_resort(recwarn): "Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)") -def test__get_fontmap(): +def test__layout(): fonts, test_str = _gen_multi_font_text() # Add some glyphs that don't exist in either font to check the Last Resort fallback. missing_glyphs = '\n几个汉字' @@ -903,11 +1013,11 @@ def test__get_fontmap(): ft = fm.get_font( fm.fontManager._find_fonts_by_props(fm.FontProperties(family=fonts)) ) - fontmap = ft._get_fontmap(test_str) - for char, font in fontmap.items(): - if char in missing_glyphs: - assert Path(font.fname).name == 'LastResortHE-Regular.ttf' - elif ord(char) > 127: - assert Path(font.fname).name == 'DejaVuSans.ttf' - else: - assert Path(font.fname).name == 'cmr10.ttf' + for substr in test_str.split('\n'): + for item in ft._layout(substr, ft2font.LoadFlags.DEFAULT): + if item.char in missing_glyphs: + assert Path(item.ft_object.fname).name == 'LastResortHE-Regular.ttf' + elif ord(item.char) > 127: + assert Path(item.ft_object.fname).name == 'DejaVuSans.ttf' + else: + assert Path(item.ft_object.fname).name == 'cmr10.ttf' diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 649e345b3613..78dacfd72907 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -50,7 +50,7 @@ def test_alpha_interp(): @image_comparison(['interp_nearest_vs_none'], tol=3.7, # For Ghostscript 10.06+. - extensions=['pdf', 'svg'], remove_text=True) + extensions=['pdf', 'svg'], remove_text=True, style='mpl20') def test_interp_nearest_vs_none(): """Test the effect of "nearest" and "none" interpolation""" # Setting dpi to something really small makes the difference very @@ -1487,7 +1487,7 @@ def test_nonuniform_logscale(): ax.add_image(im) -@image_comparison(['rgba_antialias.png'], style='mpl20', remove_text=True, tol=0.02) +@image_comparison(['rgba_antialias.png'], style='mpl20', remove_text=True) def test_rgba_antialias(): fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, sharey=False, constrained_layout=True) @@ -1741,8 +1741,8 @@ def test_non_transdata_image_does_not_touch_aspect(): assert ax.get_aspect() == 2 -@image_comparison( - ['downsampling.png'], style='mpl20', remove_text=True, tol=0.09) +@image_comparison(['downsampling.png'], style='mpl20', remove_text=True, + tol=0 if platform.machine() == 'x86_64' else 0.07) def test_downsampling(): N = 450 x = np.arange(N) / N - 0.5 @@ -1776,8 +1776,7 @@ def test_downsampling(): ax.set_title(f"interpolation='{interp}'\nspace='{space}'") -@image_comparison( - ['downsampling_speckle.png'], style='mpl20', remove_text=True, tol=0.09) +@image_comparison(['downsampling_speckle.png'], style='mpl20', remove_text=True) def test_downsampling_speckle(): fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), sharex=True, sharey=True, layout="compressed") diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index bc3bd4e97f8c..fe9405bcbdae 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -2,6 +2,7 @@ import io import itertools import platform +import sys import time from unittest import mock import warnings @@ -54,7 +55,7 @@ def test_legend_generator(): ax.legend(handles, labels, loc='upper left') -@image_comparison(['legend_auto1.png'], remove_text=True) +@image_comparison(['legend_auto1.png'], remove_text=True, style='mpl20') def test_legend_auto1(): """Test automatic legend placement""" fig, ax = plt.subplots() @@ -64,7 +65,7 @@ def test_legend_auto1(): ax.legend(loc='best') -@image_comparison(['legend_auto2.png'], remove_text=True) +@image_comparison(['legend_auto2.png'], remove_text=True, style='mpl20') def test_legend_auto2(): """Test automatic legend placement""" fig, ax = plt.subplots() @@ -74,7 +75,7 @@ def test_legend_auto2(): ax.legend([b1[0], b2[0]], ['up', 'down'], loc='best') -@image_comparison(['legend_auto3.png']) +@image_comparison(['legend_auto3.png'], style='mpl20') def test_legend_auto3(): """Test automatic legend placement""" fig, ax = plt.subplots() @@ -140,7 +141,7 @@ def test_legend_auto5(): assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds) -@image_comparison(['legend_various_labels.png'], remove_text=True) +@image_comparison(['legend_various_labels.png'], remove_text=True, style='mpl20') def test_various_labels(): # tests all sorts of label types fig = plt.figure() @@ -151,8 +152,8 @@ def test_various_labels(): ax.legend(numpoints=1, loc='best') -@image_comparison(['legend_labels_first.png'], remove_text=True, - tol=0 if platform.machine() == 'x86_64' else 0.013) +@image_comparison(['legend_labels_first.png'], remove_text=True, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.015) def test_labels_first(): # test labels to left of markers fig, ax = plt.subplots() @@ -162,8 +163,8 @@ def test_labels_first(): ax.legend(loc='best', markerfirst=False) -@image_comparison(['legend_multiple_keys.png'], remove_text=True, - tol=0 if platform.machine() == 'x86_64' else 0.013) +@image_comparison(['legend_multiple_keys.png'], remove_text=True, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.033) def test_multiple_keys(): # test legend entries with multiple keys fig, ax = plt.subplots() @@ -176,16 +177,18 @@ def test_multiple_keys(): (p2, p1): HandlerTuple(ndivide=None, pad=0)}) -@image_comparison(['rgba_alpha.png'], remove_text=True, +@image_comparison(['rgba_alpha.png'], remove_text=True, style='mpl20', tol=0 if platform.machine() == 'x86_64' else 0.03) def test_alpha_rgba(): + # This rcParam would override the explicit setting below, so disable it. + plt.rcParams['legend.framealpha'] = None fig, ax = plt.subplots() ax.plot(range(10), lw=5) leg = plt.legend(['Longlabel that will go away'], loc='center') leg.legendPatch.set_facecolor([1, 0, 0, 0.5]) -@image_comparison(['rcparam_alpha.png'], remove_text=True, +@image_comparison(['rcparam_alpha.png'], remove_text=True, style='mpl20', tol=0 if platform.machine() == 'x86_64' else 0.03) def test_alpha_rcparam(): fig, ax = plt.subplots() @@ -199,10 +202,9 @@ def test_alpha_rcparam(): leg.legendPatch.set_facecolor([1, 0, 0, 0.5]) -@image_comparison(['fancy.png'], remove_text=True, tol=0.05) +@image_comparison(['fancy.png'], remove_text=True, style='mpl20', + tol=0.01 if sys.platform == 'darwin' else 0) def test_fancy(): - # Tolerance caused by changing default shadow "shade" from 0.3 to 1 - 0.7 = - # 0.30000000000000004 # using subplot triggers some offsetbox functionality untested elsewhere plt.subplot(121) plt.plot([5] * 10, 'o--', label='XX') @@ -213,18 +215,20 @@ def test_fancy(): ncols=2, shadow=True, title="My legend", numpoints=1) -@image_comparison(['framealpha'], remove_text=True, - tol=0 if platform.machine() == 'x86_64' else 0.024) +@image_comparison(['framealpha'], remove_text=True, style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.021) def test_framealpha(): x = np.linspace(1, 100, 100) y = x plt.plot(x, y, label='mylabel', lw=10) - plt.legend(framealpha=0.5) + plt.legend(framealpha=0.5, loc='upper right') -@image_comparison(['scatter_rc3.png', 'scatter_rc1.png'], remove_text=True) +@image_comparison(['scatter_rc3.png', 'scatter_rc1.png'], remove_text=True, + style='mpl20') def test_rc(): # using subplot triggers some offsetbox functionality untested elsewhere + mpl.rcParams['legend.scatterpoints'] = 3 plt.figure() ax = plt.subplot(121) ax.scatter(np.arange(10), np.arange(10, 0, -1), label='three') @@ -239,7 +243,7 @@ def test_rc(): title="My legend") -@image_comparison(['legend_expand.png'], remove_text=True) +@image_comparison(['legend_expand.png'], remove_text=True, style='mpl20') def test_legend_expand(): """Test expand mode""" legend_modes = [None, "expand"] @@ -258,9 +262,6 @@ def test_legend_expand(): @image_comparison(['hatching'], remove_text=True, style='default') def test_hatching(): # Remove legend texts when this image is regenerated. - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() # Patches @@ -493,27 +494,27 @@ def test_figure_legend_outside(): todos += ['left ' + pos for pos in ['lower', 'center', 'upper']] todos += ['right ' + pos for pos in ['lower', 'center', 'upper']] - upperext = [20.347556, 27.722556, 790.583, 545.499] - lowerext = [20.347556, 71.056556, 790.583, 588.833] - leftext = [151.681556, 27.722556, 790.583, 588.833] - rightext = [20.347556, 27.722556, 659.249, 588.833] + upperext = [20.722556, 26.389222, 790.333, 545.16762] + lowerext = [20.722556, 70.723222, 790.333, 589.50162] + leftext = [152.056556, 26.389222, 790.333, 589.50162] + rightext = [20.722556, 26.389222, 658.999, 589.50162] axbb = [upperext, upperext, upperext, lowerext, lowerext, lowerext, leftext, leftext, leftext, rightext, rightext, rightext] - legbb = [[10., 555., 133., 590.], # upper left - [338.5, 555., 461.5, 590.], # upper center - [667, 555., 790., 590.], # upper right - [10., 10., 133., 45.], # lower left - [338.5, 10., 461.5, 45.], # lower center - [667., 10., 790., 45.], # lower right - [10., 10., 133., 45.], # left lower - [10., 282.5, 133., 317.5], # left center - [10., 555., 133., 590.], # left upper - [667, 10., 790., 45.], # right lower - [667., 282.5, 790., 317.5], # right center - [667., 555., 790., 590.]] # right upper + legbb = [[10., 554., 133., 590.], # upper left + [338.5, 554., 461.5, 590.], # upper center + [667, 554., 790., 590.], # upper right + [10., 10., 133., 46.], # lower left + [338.5, 10., 461.5, 46.], # lower center + [667., 10., 790., 46.], # lower right + [10., 10., 133., 46.], # left lower + [10., 282., 133., 318.], # left center + [10., 554., 133., 590.], # left upper + [667, 10., 790., 46.], # right lower + [667., 282., 790., 318.], # right center + [667., 554., 790., 590.]] # right upper for nn, todo in enumerate(todos): print(todo) @@ -528,8 +529,7 @@ def test_figure_legend_outside(): rtol=1e-4) -@image_comparison(['legend_stackplot.png'], - tol=0 if platform.machine() == 'x86_64' else 0.031) +@image_comparison(['legend_stackplot.png'], style='mpl20') def test_legend_stackplot(): """Test legend for PolyCollection using stackplot.""" # related to #1341, #1943, and PR #3303 @@ -587,7 +587,7 @@ def test_legend_repeatcheckok(): assert len(lab) == 2 -@image_comparison(['not_covering_scatter.png']) +@image_comparison(['not_covering_scatter.png'], style='mpl20') def test_not_covering_scatter(): colors = ['b', 'g', 'r'] @@ -599,7 +599,7 @@ def test_not_covering_scatter(): plt.gca().set_ylim(-0.5, 2.2) -@image_comparison(['not_covering_scatter_transform.png']) +@image_comparison(['not_covering_scatter_transform.png'], style='mpl20') def test_not_covering_scatter_transform(): # Offsets point to top left, the default auto position offset = mtransforms.Affine2D().translate(-20, 20) @@ -665,7 +665,7 @@ def test_empty_bar_chart_with_legend(): @image_comparison(['shadow_argument_types.png'], remove_text=True, style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.028) + tol=0.028 if sys.platform == 'darwin' else 0) def test_shadow_argument_types(): # Test that different arguments for shadow work as expected fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 39c28dc9228c..33fb8918d22a 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -78,7 +78,7 @@ r'$x+{y}^{\frac{2}{k+1}}$', r'$\frac{a}{b/2}$', r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$', - r'${a}_{0}+\frac{1}{{a}_{1}+\frac{1}{{a}_{2}+\frac{1}{{a}_{3}+\frac{1}{{a}_{4}}}}}$', + r'${a}_{0}+\dfrac{1}{{a}_{1}+\dfrac{1}{{a}_{2}+\dfrac{1}{{a}_{3}+\dfrac{1}{{a}_{4}}}}}$', r'$\binom{n}{k/2}$', r'$\binom{p}{2}{x}^{2}{y}^{p-2}-\frac{1}{1-x}\frac{1}{1-{x}^{2}}$', r'${x}^{2y}$', @@ -125,12 +125,21 @@ r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799 r'$\left(X\right)_{a}^{b}$', # github issue 7615 r'$\dfrac{\$100.00}{y}$', # github issue #1888 - r'$a=-b-c$' # github issue #28180 + r'$a=-b-c$', # github issue #28180 ] # 'svgastext' tests switch svg output to embed text as text (rather than as # paths). svgastext_math_tests = [ r'$-$-', + # Check all AutoHeightChar substitutions. + *[ + r'$\left' + lc + r' M \middle/ ? \middle\backslash ? \right' + rc + ' ' + # Normal size. + r'\left' + lc + r' \frac{M}{B} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # big size. + r'\left' + lc + r' \frac{\frac{M}{I}}{B} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # bigg size. + r'\left' + lc + r' \frac{\frac{M}{I}}{\frac{B}{U}} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # Big size. + r'\left' + lc + r'\frac{\frac{\frac{M}{I}}{N}}{\frac{\frac{B}{U}}{G}} \middle/ ? \middle\backslash ? \right' + rc + '$' # Bigg size. + for lc, rc in ['()', '[]', '<>', (r'\{', r'\}'), (r'\lfloor', r'\rfloor'), (r'\lceil', r'\rceil')] + ], ] # 'lightweight' tests test only a single fontset (dejavusans, which is the # default) and only png outputs, in order to minimize the size of baseline @@ -215,12 +224,15 @@ def baseline_images(request, fontset, index, text): @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True) -@image_comparison(baseline_images=None, - tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0) +@image_comparison( + baseline_images=None, style='mpl20', + tol=(0.013 + if platform.machine() in ('ppc64le', 's390x') or platform.system() == 'Windows' + else 0)) def test_mathtext_rendering(baseline_images, fontset, index, text): mpl.rcParams['mathtext.fontset'] = fontset fig = plt.figure(figsize=(5.25, 0.75)) - fig.text(0.5, 0.5, text, + fig.text(0.5, 0.5, text, fontsize=12, horizontalalignment='center', verticalalignment='center') @@ -229,7 +241,7 @@ def test_mathtext_rendering(baseline_images, fontset, index, text): @pytest.mark.parametrize('fontset', ['cm', 'dejavusans']) @pytest.mark.parametrize('baseline_images', ['mathtext0'], indirect=True) @image_comparison( - baseline_images=None, extensions=['svg'], + baseline_images=None, extensions=['svg'], style='mpl20', savefig_kwarg={'metadata': { # Minimize image size. 'Creator': None, 'Date': None, 'Format': None, 'Type': None}}) def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): @@ -237,7 +249,7 @@ def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): mpl.rcParams['svg.fonttype'] = 'none' # Minimize image size. fig = plt.figure(figsize=(5.25, 0.75)) fig.patch.set(visible=False) # Minimize image size. - fig.text(0.5, 0.5, text, + fig.text(0.5, 0.5, text, fontsize=16, horizontalalignment='center', verticalalignment='center') @@ -245,10 +257,10 @@ def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): ids=range(len(lightweight_math_tests))) @pytest.mark.parametrize('fontset', ['dejavusans']) @pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True) -@image_comparison(baseline_images=None, extensions=['png']) +@image_comparison(baseline_images=None, extensions=['png'], style='mpl20') def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text): fig = plt.figure(figsize=(5.25, 0.75)) - fig.text(0.5, 0.5, text, math_fontfamily=fontset, + fig.text(0.5, 0.5, text, fontsize=12, math_fontfamily=fontset, horizontalalignment='center', verticalalignment='center') @@ -257,12 +269,12 @@ def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text): @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True) -@image_comparison(baseline_images=None, extensions=['png'], +@image_comparison(baseline_images=None, extensions=['png'], style='mpl20', tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0) def test_mathfont_rendering(baseline_images, fontset, index, text): mpl.rcParams['mathtext.fontset'] = fontset fig = plt.figure(figsize=(5.25, 0.75)) - fig.text(0.5, 0.5, text, + fig.text(0.5, 0.5, text, fontsize=12, horizontalalignment='center', verticalalignment='center') @@ -391,7 +403,7 @@ def test_operator_space(fig_test, fig_ref): fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$") fig_test.text(0.1, 0.7, r"$\cos^2$") fig_test.text(0.1, 0.8, r"$\log_2$") - fig_test.text(0.1, 0.9, r"$\sin^2 \cos$") # GitHub issue #17852 + fig_test.text(0.1, 0.9, r"$\sin^2 \max \cos$") # GitHub issue #17852 fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$") fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$") @@ -401,7 +413,7 @@ def test_operator_space(fig_test, fig_ref): fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$") fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$") fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$") - fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,cos}$") + fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,max} \mathrm{\,cos}$") @check_figures_equal() @@ -468,7 +480,7 @@ def test_math_to_image(tmp_path): @image_comparison(baseline_images=['math_fontfamily_image.png'], - savefig_kwarg={'dpi': 40}) + savefig_kwarg={'dpi': 40}, style='mpl20') def test_math_fontfamily(): fig = plt.figure(figsize=(10, 3)) fig.text(0.2, 0.7, r"$This\ text\ should\ have\ one\ font$", @@ -562,20 +574,43 @@ def test_boldsymbol(fig_test, fig_ref): fig_ref.text(0.1, 0.2, r"$\mathrm{abc0123\alpha}$") +@check_figures_equal() +def test_mathnormal(fig_test, fig_ref): + # ensure that \mathnormal is parsed and sets digits upright + fig_test.text(0.1, 0.2, r"$\mathnormal{0123456789}$") + fig_ref.text(0.1, 0.2, r"$\mathrm{0123456789}$") + + +# Test vector output because in raster output some minor differences remain, +# likely due to double-striking. +@check_figures_equal(extensions=["pdf"]) +def test_phantoms(fig_test, fig_ref): + fig_test.text(0.5, 0.9, r"$\rlap{rlap}extra$", ha="left") + fig_ref.text(0.5, 0.9, r"$rlap$", ha="left") + fig_ref.text(0.5, 0.9, r"$extra$", ha="left") + + fig_test.text(0.5, 0.8, r"$extra\llap{llap}$", ha="right") + fig_ref.text(0.5, 0.8, r"$llap$", ha="right") + fig_ref.text(0.5, 0.8, r"$extra$", ha="right") + + fig_test.text(0.5, 0.7, r"$\phantom{phantom}$") + + def test_box_repr(): s = repr(_mathtext.Parser().parse( r"$\frac{1}{2}$", _mathtext.DejaVuSansFonts(fm.FontProperties(), LoadFlags.NO_HINTING), fontsize=12, dpi=100)) assert s == textwrap.dedent("""\ - Hlist[ + Hlist[ Hlist[], - Hlist[ - Hlist[ - Vlist[ - HCentered[ + Hlist[ + Hlist[ + Hbox, + Vlist[ + HCentered[ Glue, - Hlist[ + Hlist[ `1`, k2.36, ], @@ -584,9 +619,9 @@ def test_box_repr(): Vbox, Hrule, Vbox, - HCentered[ + HCentered[ Glue, - Hlist[ + Hlist[ `2`, k2.02, ], diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 80dcc43894c4..12a12cf3e90d 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -241,7 +241,7 @@ def test_negative_rect(): assert_array_equal(np.roll(neg_vertices, 2, 0), pos_vertices) -@image_comparison(['clip_to_bbox.png']) +@image_comparison(['clip_to_bbox.png'], style='mpl20') def test_clip_to_bbox(): fig, ax = plt.subplots() ax.set_xlim([-18, 20]) @@ -550,7 +550,7 @@ def test_multi_color_hatch(): ax.add_patch(r) -@image_comparison(['units_rectangle.png']) +@image_comparison(['units_rectangle.png'], style='mpl20') def test_units_rectangle(): import matplotlib.testing.jpl_units as U U.register() @@ -813,7 +813,7 @@ def test_boxstyle_errors(fmt, match): BoxStyle(fmt) -@image_comparison(['annulus.png']) +@image_comparison(['annulus.png'], style='mpl20') def test_annulus(): fig, ax = plt.subplots() @@ -825,7 +825,7 @@ def test_annulus(): ax.set_aspect('equal') -@image_comparison(['annulus.png']) +@image_comparison(['annulus.png'], style='mpl20') def test_annulus_setters(): fig, ax = plt.subplots() @@ -846,7 +846,7 @@ def test_annulus_setters(): ell.angle = 45 -@image_comparison(['annulus.png']) +@image_comparison(['annulus.png'], style='mpl20') def test_annulus_setters2(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py index d957ef2a5510..7095f6b3855b 100644 --- a/lib/matplotlib/tests/test_patheffects.py +++ b/lib/matplotlib/tests/test_patheffects.py @@ -1,4 +1,4 @@ -import platform +import sys import numpy as np @@ -11,7 +11,7 @@ from matplotlib.patheffects import PathEffectRenderer -@image_comparison(['patheffect1'], remove_text=True) +@image_comparison(['patheffect1'], remove_text=True, style='mpl20') def test_patheffect1(): ax1 = plt.subplot() ax1.imshow([[1, 2], [2, 3]]) @@ -30,7 +30,7 @@ def test_patheffect1(): @image_comparison(['patheffect2'], remove_text=True, style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.06) + tol=0.051 if sys.platform == 'darwin' else 0) def test_patheffect2(): ax2 = plt.subplot() @@ -45,9 +45,10 @@ def test_patheffect2(): foreground="w")]) -@image_comparison(['patheffect3'], - tol=0 if platform.machine() == 'x86_64' else 0.019) +@image_comparison(['patheffect3'], style='mpl20', + tol=0.02 if sys.platform == 'darwin' else 0) def test_patheffect3(): + plt.figure(figsize=(8, 6)) p1, = plt.plot([1, 3, 5, 4, 3], 'o-b', lw=4) p1.set_path_effects([path_effects.SimpleLineShadow(), path_effects.Normal()]) @@ -74,7 +75,7 @@ def test_patheffect3(): t.set_path_effects(pe) -@image_comparison(['stroked_text.png']) +@image_comparison(['stroked_text.png'], style='mpl20') def test_patheffects_stroked_text(): text_chunks = [ 'A B C D E F G H I J K L', @@ -87,7 +88,7 @@ def test_patheffects_stroked_text(): ] font_size = 50 - ax = plt.axes((0, 0, 1, 1)) + ax = plt.figure(figsize=(8, 6)).add_axes((0, 0, 1, 1)) for i, chunk in enumerate(text_chunks): text = ax.text(x=0.01, y=(0.9 - i * 0.13), s=chunk, fontdict={'ha': 'left', 'va': 'center', @@ -120,7 +121,7 @@ def test_SimplePatchShadow_offset(): assert pe._offset == (4, 5) -@image_comparison(['collection'], tol=0.03, style='mpl20') +@image_comparison(['collection'], tol=0.032, style='mpl20') def test_collection(): x, y = np.meshgrid(np.linspace(0, 10, 150), np.linspace(-5, 5, 100)) data = np.sin(x) + np.cos(y) @@ -186,7 +187,7 @@ def test_tickedstroke(text_placeholders): ax3.set_ylim(0, 4) -@image_comparison(['spaces_and_newlines.png'], remove_text=True) +@image_comparison(['spaces_and_newlines.png'], remove_text=True, style='mpl20') def test_patheffects_spaces_and_newlines(): ax = plt.subplot() s1 = " " diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index f688f384479b..a805fb61d238 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -1,3 +1,5 @@ +import sys + import numpy as np from numpy.testing import assert_allclose import pytest @@ -9,7 +11,8 @@ import matplotlib.ticker as mticker -@image_comparison(['polar_axes.png'], style='default', tol=0.012) +@image_comparison(['polar_axes.png'], style='default', + tol=0.009 if sys.platform == 'darwin' else 0) def test_polar_annotations(): # You can specify the xypoint and the xytext in different positions and # coordinate systems, and optionally turn on a connecting line and mark the @@ -44,7 +47,7 @@ def test_polar_annotations(): @image_comparison(['polar_coords.png'], style='default', remove_text=True, - tol=0.014) + tol=0.013 if sys.platform == 'darwin' else 0) def test_polar_coord_annotations(): # You can also use polar notation on a cartesian axes. Here the native # coordinate system ('data') is cartesian, so you need to specify the @@ -72,7 +75,7 @@ def test_polar_coord_annotations(): ax.set_ylim(-20, 20) -@image_comparison(['polar_alignment.png']) +@image_comparison(['polar_alignment.png'], style='mpl20') def test_polar_alignment(): # Test changing the vertical/horizontal alignment of a polar graph. angles = np.arange(0, 360, 90) @@ -214,8 +217,7 @@ def test_polar_theta_position(): ax.set_theta_direction('clockwise') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['polar_rlabel_position.png'], style='default', tol=0.07) +@image_comparison(['polar_rlabel_position.png'], style='default') def test_polar_rlabel_position(): fig = plt.figure() ax = fig.add_subplot(projection='polar') @@ -230,8 +232,7 @@ def test_polar_title_position(): ax.set_title('foo') -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['polar_theta_wedge.png'], style='default', tol=0.2) +@image_comparison(['polar_theta_wedge.png'], style='default') def test_polar_theta_limits(): r = np.arange(0, 3.0, 0.01) theta = 2*np.pi*r @@ -332,7 +333,7 @@ def test_get_tightbbox_polar(): fig.canvas.draw() bb = ax.get_tightbbox(fig.canvas.get_renderer()) assert_allclose( - bb.extents, [107.7778, 29.2778, 539.7847, 450.7222], rtol=1e-03) + bb.extents, [108.27778, 29.1111, 539.7222, 450.8889], rtol=1e-03) @check_figures_equal() diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index ef4d7a0598eb..4784a7e4dc42 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -101,16 +101,16 @@ def test_zero_headlength(): fig.canvas.draw() # Check that no warning is emitted. -@image_comparison(['quiver_animated_test_image.png']) +@image_comparison(['quiver_animated_test_image.png'], style='mpl20') def test_quiver_animate(): # Tests fix for #2616 fig, ax = plt.subplots() Q = draw_quiver(ax, animated=True) - ax.quiverkey(Q, 0.5, 0.92, 2, r'$2 \frac{m}{s}$', + ax.quiverkey(Q, 0.5, 0.88, 2, r'$2 \frac{m}{s}$', labelpos='W', fontproperties={'weight': 'bold'}) -@image_comparison(['quiver_with_key_test_image.png']) +@image_comparison(['quiver_with_key_test_image.png'], style='mpl20') def test_quiver_with_key(): fig, ax = plt.subplots() ax.margins(0.1) @@ -138,7 +138,7 @@ def test_quiver_copy(): assert q0.V[0] == 2.0 -@image_comparison(['quiver_key_pivot.png'], remove_text=True) +@image_comparison(['quiver_key_pivot.png'], remove_text=True, style='mpl20') def test_quiver_key_pivot(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py index 98d3728b1d34..6b2e5b4cd301 100644 --- a/lib/matplotlib/tests/test_simplification.py +++ b/lib/matplotlib/tests/test_simplification.py @@ -493,7 +493,7 @@ def test_para_equal_perp(): ax.plot(x + 1, y + 1, 'ro') -@image_comparison(['clipping_with_nans']) +@image_comparison(['clipping_with_nans'], style='mpl20') def test_clipping_with_nans(): x = np.linspace(0, 3.14 * 2, 3000) y = np.sin(x) diff --git a/lib/matplotlib/tests/test_spines.py b/lib/matplotlib/tests/test_spines.py index 5aecf6c2ad55..4945c53d904d 100644 --- a/lib/matplotlib/tests/test_spines.py +++ b/lib/matplotlib/tests/test_spines.py @@ -55,7 +55,7 @@ def set_val(self, val): spines['top':] -@image_comparison(['spines_axes_positions.png']) +@image_comparison(['spines_axes_positions.png'], style='mpl20') def test_spines_axes_positions(): # SF bug 2852168 fig = plt.figure() @@ -72,7 +72,7 @@ def test_spines_axes_positions(): ax.spines.bottom.set_color('none') -@image_comparison(['spines_data_positions.png']) +@image_comparison(['spines_data_positions.png'], style='mpl20') def test_spines_data_positions(): fig, ax = plt.subplots() ax.spines.left.set_position(('data', -1.5)) @@ -81,6 +81,8 @@ def test_spines_data_positions(): ax.spines.bottom.set_position('zero') ax.set_xlim([-2, 2]) ax.set_ylim([-2, 2]) + ax.xaxis.set_ticks_position('both') + ax.yaxis.set_ticks_position('both') @check_figures_equal() diff --git a/lib/matplotlib/tests/test_subplots.py b/lib/matplotlib/tests/test_subplots.py index 0f00a88aa72d..ed07b0226bc8 100644 --- a/lib/matplotlib/tests/test_subplots.py +++ b/lib/matplotlib/tests/test_subplots.py @@ -180,11 +180,11 @@ def test_exceptions(): plt.subplots(2, 2, sharey='blah') -@image_comparison(['subplots_offset_text.png'], +@image_comparison(['subplots_offset_text.png'], style='mpl20', tol=0 if platform.machine() == 'x86_64' else 0.028) def test_subplots_offsettext(): x = np.arange(0, 1e10, 1e9) - y = np.arange(0, 100, 10)+1e4 + y = np.arange(0, 100, 10)+1e5 fig, axs = plt.subplots(2, 2, sharex='col', sharey='all') axs[0, 0].plot(x, x) axs[1, 0].plot(x, x) diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index 43b8702737a6..304e69322f81 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -17,7 +17,7 @@ def test_non_square(): plt.table(cellColours=cellcolors) -@image_comparison(['table_zorder.png'], remove_text=True) +@image_comparison(['table_zorder.png'], remove_text=True, style='mpl20') def test_zorder(): data = [[66386, 174296], [58230, 381139]] @@ -50,7 +50,7 @@ def test_zorder(): plt.yticks([]) -@image_comparison(['table_labels.png']) +@image_comparison(['table_labels.png'], style='mpl20') def test_label_colours(): dim = 3 @@ -123,7 +123,7 @@ def test_customcell(): assert c == code -@image_comparison(['table_auto_column.png']) +@image_comparison(['table_auto_column.png'], style='mpl20') def test_auto_column(): fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 37fee446df5c..c1cd22a9a2f3 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -14,7 +14,7 @@ from matplotlib.backend_bases import MouseEvent from matplotlib.backends.backend_agg import RendererAgg from matplotlib.figure import Figure -from matplotlib.font_manager import FontProperties +from matplotlib.font_manager import FontProperties, fontManager, get_font import matplotlib.patches as mpatches import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec @@ -26,7 +26,7 @@ pyparsing_version = parse_version(pyparsing.__version__) -@image_comparison(['font_styles']) +@image_comparison(['font_styles'], style='mpl20') def test_font_styles(): def find_matplotlib_font(**kw): @@ -115,7 +115,27 @@ def find_matplotlib_font(**kw): ax.set_yticks([]) -@image_comparison(['multiline']) +@image_comparison(['complex'], extensions=['png', 'pdf', 'svg', 'eps'], style='mpl20') +def test_complex_shaping(): + # Raqm is Arabic for writing; note that because Arabic is RTL, the characters here + # may seem to be in a different order than expected, but libraqm will order them + # correctly for us. + text = ( + 'Arabic: \N{Arabic Letter REH}\N{Arabic FATHA}\N{Arabic Letter QAF}' + '\N{Arabic SUKUN}\N{Arabic Letter MEEM}') + math_signs = '\N{N-ary Product}\N{N-ary Coproduct}\N{N-ary summation}\N{Integral}' + text = math_signs + text + math_signs + fig = plt.figure(figsize=(6, 2)) + fig.text(0.5, 0.75, text, size=32, ha='center', va='center') + # Also check fallback behaviour: + # - English should use cmr10 + # - Math signs should use DejaVu Sans Display (and thus be larger than the rest) + # - Arabic should use DejaVu Sans + fig.text(0.5, 0.25, text, size=32, ha='center', va='center', + family=['cmr10', 'DejaVu Sans Display', 'DejaVu Sans']) + + +@image_comparison(['multiline'], style='mpl20') def test_multiline(): plt.figure() ax = plt.subplot(1, 1, 1) @@ -139,12 +159,8 @@ def test_multiline(): ax.set_yticks([]) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['multiline2'], style='mpl20', tol=0.05) +@image_comparison(['multiline2'], style='mpl20') def test_multiline2(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.set_xlim(0, 1.4) @@ -211,10 +227,9 @@ def test_antialiasing(): mpl.rcParams['text.antialiased'] = False # Should not affect existing text. -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['text_contains.png'], tol=0.05) +@image_comparison(['text_contains.png'], style='mpl20') def test_contains(): - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) ax = plt.axes() mevent = MouseEvent('button_press_event', fig.canvas, 0.5, 0.5, 1, None) @@ -270,7 +285,7 @@ def test_annotate_errors(err, xycoords, match): fig.canvas.draw() -@image_comparison(['titles']) +@image_comparison(['titles'], style='mpl20') def test_titles(): # left and right side titles plt.figure() @@ -281,8 +296,7 @@ def test_titles(): ax.set_yticks([]) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['text_alignment'], style='mpl20', tol=0.08) +@image_comparison(['text_alignment'], style='mpl20') def test_alignment(): plt.figure() ax = plt.subplot(1, 1, 1) @@ -306,7 +320,23 @@ def test_alignment(): ax.set_yticks([]) -@image_comparison(['axes_titles.png']) +@image_comparison(baseline_images=['rotation_anchor.png'], style='mpl20', + remove_text=True) +def test_rotation_mode_anchor(): + fig, ax = plt.subplots() + + ax.plot([0, 1], lw=0) + ax.axvline(.5, linewidth=.5, color='.5') + ax.axhline(.5, linewidth=.5, color='.5') + + N = 4 + for r in range(N): + ax.text(.5, .5, 'pP', color=f'C{r}', size=100, + rotation=r/N*360, rotation_mode='anchor', + verticalalignment='center_baseline') + + +@image_comparison(['axes_titles.png'], style='mpl20') def test_axes_titles(): # Related to issue #3327 plt.figure() @@ -432,14 +462,14 @@ def test_null_rotation_with_rotation_mode(ha, va): t1.get_window_extent(fig.canvas.renderer).get_points()) -@image_comparison(['text_bboxclip']) +@image_comparison(['text_bboxclip'], style='mpl20') def test_bbox_clipping(): plt.text(0.9, 0.2, 'Is bbox clipped?', backgroundcolor='r', clip_on=True) t = plt.text(0.9, 0.5, 'Is fancy bbox clipped?', clip_on=True) t.set_bbox({"boxstyle": "round, pad=0.1"}) -@image_comparison(['annotation_negative_ax_coords.png']) +@image_comparison(['annotation_negative_ax_coords.png'], style='mpl20') def test_annotation_negative_ax_coords(): fig, ax = plt.subplots() @@ -467,31 +497,31 @@ def test_annotation_negative_ax_coords(): va='top') -@image_comparison(['annotation_negative_fig_coords.png']) +@image_comparison(['annotation_negative_fig_coords.png'], style='mpl20') def test_annotation_negative_fig_coords(): fig, ax = plt.subplots() ax.annotate('+ pts', - xytext=[10, 120], textcoords='figure points', - xy=[10, 120], xycoords='figure points', fontsize=32) + xytext=[10, 250], textcoords='figure points', + xy=[10, 250], xycoords='figure points', fontsize=32) ax.annotate('- pts', - xytext=[-10, 180], textcoords='figure points', - xy=[-10, 180], xycoords='figure points', fontsize=32, + xytext=[-10, 310], textcoords='figure points', + xy=[-10, 310], xycoords='figure points', fontsize=32, va='top') ax.annotate('+ frac', - xytext=[0.05, 0.55], textcoords='figure fraction', - xy=[0.05, 0.55], xycoords='figure fraction', fontsize=32) + xytext=[0.05, 0.5], textcoords='figure fraction', + xy=[0.05, 0.5], xycoords='figure fraction', fontsize=32) ax.annotate('- frac', - xytext=[-0.05, 0.5], textcoords='figure fraction', - xy=[-0.05, 0.5], xycoords='figure fraction', fontsize=32, + xytext=[-0.05, 0.45], textcoords='figure fraction', + xy=[-0.05, 0.45], xycoords='figure fraction', fontsize=32, va='top') ax.annotate('+ pixels', xytext=[50, 50], textcoords='figure pixels', xy=[50, 50], xycoords='figure pixels', fontsize=32) ax.annotate('- pixels', - xytext=[-50, 100], textcoords='figure pixels', - xy=[-50, 100], xycoords='figure pixels', fontsize=32, + xytext=[-50, 150], textcoords='figure pixels', + xy=[-50, 150], xycoords='figure pixels', fontsize=32, va='top') @@ -518,7 +548,7 @@ def test_text_stale(): assert not fig.stale -@image_comparison(['agg_text_clip.png']) +@image_comparison(['agg_text_clip.png'], style='mpl20') def test_agg_text_clip(): np.random.seed(1) fig, (ax1, ax2) = plt.subplots(2) @@ -536,7 +566,7 @@ def test_text_size_binding(): assert sz1 == fp.get_size_in_points() -@image_comparison(['font_scaling.pdf']) +@image_comparison(['font_scaling.pdf'], style='mpl20') def test_font_scaling(): mpl.rcParams['pdf.fonttype'] = 42 fig, ax = plt.subplots(figsize=(6.4, 12.4)) @@ -683,8 +713,6 @@ def test_annotation_units(fig_test, fig_ref): @image_comparison(['large_subscript_title.png'], style='mpl20') def test_large_subscript_title(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 plt.rcParams['axes.titley'] = None fig, axs = plt.subplots(1, 2, figsize=(9, 2.5), constrained_layout=True) @@ -721,7 +749,7 @@ def test_wrap(x, rotation, halign): def test_mathwrap(): - fig = plt.figure(figsize=(6, 4)) + fig = plt.figure(figsize=(5, 4)) s = r'This is a very $\overline{\mathrm{long}}$ line of Mathtext.' text = fig.text(0, 0.5, s, size=40, wrap=True) fig.canvas.draw() @@ -814,24 +842,6 @@ def test_invalid_color(): plt.figtext(.5, .5, "foo", c="foobar") -@image_comparison(['text_pdf_kerning.pdf'], style='mpl20') -def test_pdf_kerning(): - plt.figure() - plt.figtext(0.1, 0.5, "ATATATATATATATATATA", size=30) - - -def test_unsupported_script(recwarn): - fig = plt.figure() - t = fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}") - fig.canvas.draw() - assert all(isinstance(warn.message, UserWarning) for warn in recwarn) - assert ( - [warn.message.args for warn in recwarn] == - [(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from font(s) " - + f"{t.get_fontname()}.",), - (r"Matplotlib currently does not support Bengali natively.",)]) - - # See gh-26152 for more information on this xfail @pytest.mark.xfail(pyparsing_version.release == (3, 1, 0), reason="Error messages are incorrect with pyparsing 3.1.0") @@ -862,21 +872,6 @@ def test_parse_math_rcparams(): fig.canvas.draw() -@image_comparison(['text_pdf_font42_kerning.pdf'], style='mpl20') -def test_pdf_font42_kerning(): - plt.rcParams['pdf.fonttype'] = 42 - plt.figure() - plt.figtext(0.1, 0.5, "ATAVATAVATAVATAVATA", size=30) - - -@image_comparison(['text_pdf_chars_beyond_bmp.pdf'], style='mpl20') -def test_pdf_chars_beyond_bmp(): - plt.rcParams['pdf.fonttype'] = 42 - plt.rcParams['mathtext.fontset'] = 'stixsans' - plt.figure() - plt.figtext(0.1, 0.5, "Mass $m$ \U00010308", size=30) - - @needs_usetex def test_metrics_cache(): # dig into the signature to get the mutable default used as a cache @@ -1042,8 +1037,16 @@ def test_text_annotation_get_window_extent(): _, _, d = renderer.get_text_width_height_descent( 'text', annotation._fontproperties, ismath=False) - _, _, lp_d = renderer.get_text_width_height_descent( - 'lp', annotation._fontproperties, ismath=False) + font = get_font(fontManager._find_fonts_by_props(annotation._fontproperties)) + for name, key in [('OS/2', 'sTypoDescender'), ('hhea', 'descent')]: + if (table := font.get_sfnt_table(name)) is not None: + units_per_em = font.get_sfnt_table('head')['unitsPerEm'] + fontsize = annotation._fontproperties.get_size_in_points() + lp_d = -table[key] / units_per_em * fontsize * figure.dpi / 72 + break + else: + _, _, lp_d = renderer.get_text_width_height_descent( + 'lp', annotation._fontproperties, ismath=False) below_line = max(d, lp_d) # These numbers are specific to the current implementation of Text @@ -1082,7 +1085,7 @@ def test_text_with_arrow_annotation_get_window_extent(): assert bbox.width == text_bbox.width + 50.0 # make sure the annotation text bounding box is same size # as the bounding box of the same string as a Text object - assert ann_txt_bbox.height == text_bbox.height + assert_almost_equal(ann_txt_bbox.height, text_bbox.height) assert ann_txt_bbox.width == text_bbox.width # compute the expected bounding box of arrow + text expected_bbox = mtransforms.Bbox.union([ann_txt_bbox, arrow_bbox]) @@ -1135,10 +1138,9 @@ def test_empty_annotation_get_window_extent(): assert points[0, 1] == 50.0 -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['basictext_wrap.png'], tol=0.3) +@image_comparison(['basictext_wrap.png'], style='mpl20') def test_basic_wrap(): - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) plt.axis([0, 10, 0, 10]) t = "This is a really long string that I'd rather have wrapped so that" \ " it doesn't go outside of the figure, but if it's long enough it" \ @@ -1152,10 +1154,9 @@ def test_basic_wrap(): plt.text(-1, 0, t, ha='left', rotation=-15, wrap=True) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['fonttext_wrap.png'], tol=0.3) +@image_comparison(['fonttext_wrap.png'], style='mpl20') def test_font_wrap(): - fig = plt.figure() + fig = plt.figure(figsize=(8, 6)) plt.axis([0, 10, 0, 10]) t = "This is a really long string that I'd rather have wrapped so that" \ " it doesn't go outside of the figure, but if it's long enough it" \ @@ -1185,9 +1186,7 @@ def test_va_for_angle(): assert alignment in ['center', 'top', 'baseline'] -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['xtick_rotation_mode.png'], remove_text=False, style='mpl20', - tol=0.3) +@image_comparison(['xtick_rotation_mode.png'], remove_text=False, style='mpl20') def test_xtick_rotation_mode(): fig, ax = plt.subplots(figsize=(12, 1)) ax.set_yticks([]) @@ -1206,9 +1205,7 @@ def test_xtick_rotation_mode(): plt.subplots_adjust(left=0.01, right=0.99, top=.6, bottom=.4) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['ytick_rotation_mode.png'], remove_text=False, style='mpl20', - tol=0.3) +@image_comparison(['ytick_rotation_mode.png'], remove_text=False, style='mpl20') def test_ytick_rotation_mode(): fig, ax = plt.subplots(figsize=(1, 12)) ax.set_xticks([]) @@ -1236,3 +1233,78 @@ def test_text_tightbbox_outside_scale_domain(): invalid_text = ax.text(0, -5, 'invalid') invalid_bbox = invalid_text.get_tightbbox(fig.canvas.get_renderer()) assert not np.isfinite(invalid_bbox.width) + + +@image_comparison(['features'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) +def test_text_features(): + fig = plt.figure(figsize=(5, 1.5)) + t = fig.text(1, 0.7, 'Default: fi ffi fl st', + fontsize=32, horizontalalignment='right') + assert t.get_fontfeatures() is None + t = fig.text(1, 0.4, 'Disabled: fi ffi fl st', + fontsize=32, horizontalalignment='right', + fontfeatures=['-liga']) + assert t.get_fontfeatures() == ('-liga', ) + t = fig.text(1, 0.1, 'Discretionary: fi ffi fl st', + fontsize=32, horizontalalignment='right') + t.set_fontfeatures(['dlig']) + assert t.get_fontfeatures() == ('dlig', ) + + +@pytest.mark.parametrize( + 'input, match', + [ + ([1, 2, 3], 'must be list of tuple'), + ([(1, 2)], 'must be list of tuple'), + ([('en', 'foo', 2)], 'start location must be int'), + ([('en', 1, 'foo')], 'end location must be int'), + ], +) +def test_text_language_invalid(input, match): + with pytest.raises(TypeError, match=match): + Text(0, 0, 'foo', language=input) + + +@image_comparison(['language'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) +def test_text_language(): + fig = plt.figure(figsize=(5, 3)) + + t = fig.text(0, 0.8, 'Default', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.55, 'Lang A', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.3, 'Lang B', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.05, 'Mixed', fontsize=32) + assert t.get_language() is None + + # DejaVu Sans supports language-specific glyphs in the Serbian and Macedonian + # languages in the Cyrillic alphabet. + cyrillic = '\U00000431' + t = fig.text(0.4, 0.8, cyrillic, fontsize=32) + assert t.get_language() is None + t = fig.text(0.4, 0.55, cyrillic, fontsize=32, language='sr') + assert t.get_language() == 'sr' + t = fig.text(0.4, 0.3, cyrillic, fontsize=32) + t.set_language('ru') + assert t.get_language() == 'ru' + t = fig.text(0.4, 0.05, cyrillic * 4, fontsize=32, + language=[('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)]) + assert t.get_language() == (('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)) + + # Or the Sámi family of languages in the Latin alphabet. + latin = '\U0000014a' + t = fig.text(0.7, 0.8, latin, fontsize=32) + assert t.get_language() is None + with plt.rc_context({'text.language': 'en'}): + t = fig.text(0.7, 0.55, latin, fontsize=32) + assert t.get_language() == 'en' + t = fig.text(0.7, 0.3, latin, fontsize=32, language='smn') + assert t.get_language() == 'smn' + # Tuples are not documented, but we'll allow it. + t = fig.text(0.7, 0.05, latin * 4, fontsize=32) + t.set_language((('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4))) + assert t.get_language() == ( + ('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4)) diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index ae065a231fd9..c9187915b5a2 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -232,7 +232,7 @@ def tris_contain_point(triang, xy): triang = mtri.Triangulation(tri_points[1:, 0], tri_points[1:, 1]) -@image_comparison(['tripcolor1.png']) +@image_comparison(['tripcolor1.png'], style='mpl20') def test_tripcolor(): x = np.asarray([0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0.75]) y = np.asarray([0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 0.75]) diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index c13c54a101fc..d2350667e94f 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -80,9 +80,8 @@ def default_units(value, axis): # Tests that the conversion machinery works properly for classes that # work as a facade over numpy arrays (like pint) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['plot_pint.png'], style='mpl20', - tol=0.03 if platform.machine() == 'x86_64' else 0.04) + tol=0 if platform.machine() == 'x86_64' else 0.03) def test_numpy_facade(quantity_converter): # use former defaults to match existing baseline image plt.rcParams['axes.formatter.limits'] = -7, 7 @@ -143,9 +142,8 @@ def test_jpl_bar_units(): ax.set_ylim([b - 1 * day, b + w[-1] + (1.001) * day]) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['jpl_barh_units.png'], - savefig_kwarg={'dpi': 120}, style='mpl20', tol=0.02) + savefig_kwarg={'dpi': 120}, style='mpl20') def test_jpl_barh_units(): import matplotlib.testing.jpl_units as units units.register() diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index 78d9fd6cc948..87277f152789 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -66,7 +66,7 @@ def test_mathdefault(): fig.canvas.draw() -@image_comparison(['eqnarray.png']) +@image_comparison(['eqnarray.png'], style='mpl20') def test_multiline_eqnarray(): text = ( r'\begin{eqnarray*}' @@ -226,9 +226,8 @@ def test_pdf_type1_font_subsetting(): _old_gs_version = True -# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['rotation'], extensions=['eps', 'pdf', 'png', 'svg'], - style='mpl20', tol=3.91 if _old_gs_version else 0.2) + style='mpl20', tol=3.91 if _old_gs_version else 0) def test_rotation(): mpl.rcParams['text.usetex'] = True diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 2e870f050209..9c6478f9c7df 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2,7 +2,9 @@ Classes for including text in a figure. """ +from collections.abc import Sequence import functools +import itertools import logging import math from numbers import Real @@ -13,7 +15,7 @@ import matplotlib as mpl from . import _api, artist, cbook, _docstring, colors as mcolors from .artist import Artist -from .font_manager import FontProperties +from .font_manager import FontProperties, fontManager, get_font from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle from .textpath import TextPath, TextToPath # noqa # Logically located here from .transforms import ( @@ -23,43 +25,23 @@ _log = logging.getLogger(__name__) -def _get_textbox(text, renderer): +@functools.lru_cache(maxsize=128) +def _rotate(theta): """ - Calculate the bounding box of the text. - - The bbox position takes text rotation into account, but the width and - height are those of the unrotated box (unlike `.Text.get_window_extent`). + Return an Affine2D object that rotates by the given angle in radians. """ - # TODO : This function may move into the Text class as a method. As a - # matter of fact, the information from the _get_textbox function - # should be available during the Text._get_layout() call, which is - # called within the _get_textbox. So, it would be better to move this - # function as a method with some refactoring of _get_layout method. - - projected_xs = [] - projected_ys = [] - - theta = np.deg2rad(text.get_rotation()) - tr = Affine2D().rotate(-theta) - - _, parts, d = text._get_layout(renderer) - - for t, wh, x, y in parts: - w, h = wh - - xt1, yt1 = tr.transform((x, y)) - yt1 -= d - xt2, yt2 = xt1 + w, yt1 + h - - projected_xs.extend([xt1, xt2]) - projected_ys.extend([yt1, yt2]) + return Affine2D().rotate(theta) - xt_box, yt_box = min(projected_xs), min(projected_ys) - w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box - x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box)) - - return x_box, y_box, w_box, h_box +def _rotate_point(angle, x, y): + """ + Rotate point (x, y) by rotation angle in degrees + """ + if angle == 0: + return (x, y) + angle_rad = math.radians(angle) + cos, sin = math.cos(angle_rad), math.sin(angle_rad) + return (cos * x - sin * y, sin * x + cos * y) def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): @@ -208,6 +190,8 @@ def __init__(self, super().__init__() self._x, self._y = x, y self._text = '' + self._features = None + self.set_language(None) self._reset_visual_defaults( text=text, color=color, @@ -257,7 +241,7 @@ def _reset_visual_defaults( self._bbox_patch = None # a FancyBboxPatch instance self._renderer = None if linespacing is None: - linespacing = 1.2 # Maybe use rcParam later. + linespacing = 'normal' # Maybe use rcParam later. self.set_linespacing(linespacing) self.set_rotation_mode(rotation_mode) self.set_antialiased(mpl._val_or_rc(antialiased, 'text.antialiased')) @@ -353,10 +337,10 @@ def _char_index_at(self, x): return (np.abs(size_accum - std_x)).argmin() def get_rotation(self): - """Return the text angle in degrees between 0 and 360.""" + """Return the text angle in degrees in the range [0, 360).""" if self.get_transform_rotates_text(): return self.get_transform().transform_angles( - [self._rotation], [self.get_unitless_position()]).item(0) + [self._rotation], [self.get_unitless_position()]).item(0) % 360 else: return self._rotation @@ -431,26 +415,65 @@ def update_from(self, other): def _get_layout(self, renderer): """ - Return the extent (bbox) of the text together with - multiple-alignment information. Note that it returns an extent - of a rotated text when necessary. + Return + + - the rotated, axis-aligned text bbox; + - a list of ``(line, (width, ascent, descent), xy)`` tuples for each line; + - a ``(xy, (width, height))` pair of the lower-left corner and size of the + rotated, *text-aligned* text box (i.e. describing how to draw the + text-surrounding box). """ thisx, thisy = 0.0, 0.0 lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty. - ws = [] - hs = [] + # Reminder: The ascent (a) goes from the baseline to the top and the + # descent (d) from the baseline to the bottom; both are (typically) + # nonnegative. The height h is the sum, h = a + d. + wads = [] # (width, ascents, descents) xs = [] ys = [] - # Full vertical extent of font, including ascenders and descenders: - _, lp_h, lp_d = _get_text_metrics_with_cache( - renderer, "lp", self._fontproperties, - ismath="TeX" if self.get_usetex() else False, - dpi=self.get_figure(root=True).dpi) - min_dy = (lp_h - lp_d) * self._linespacing - - for i, line in enumerate(lines): + min_ascent = min_descent = line_gap = None + dpi = self.get_figure(root=True).dpi + # Determine full vertical extent of font, including ascenders and descenders: + if not self.get_usetex(): + if hasattr(renderer, '_get_font_height_metrics'): + # TODO: This is a temporary internal method call (for _backend_pdf_ps to + # support AFM files) until we design a proper API for the backends. + min_ascent, min_descent, line_gap = renderer._get_font_height_metrics( + self._fontproperties) + if min_ascent is None: + font = get_font(fontManager._find_fonts_by_props(self._fontproperties)) + possible = [ + ('OS/2', 'sTypoLineGap', 'sTypoAscender', 'sTypoDescender'), + ('hhea', 'lineGap', 'ascent', 'descent') + ] + for table_name, linegap_key, ascent_key, descent_key in possible: + table = font.get_sfnt_table(table_name) + if table is None: + continue + # Rescale to font size/DPI if the metrics were available. + fontsize = self._fontproperties.get_size_in_points() + units_per_em = font.get_sfnt_table('head')['unitsPerEm'] + scale = 1 / units_per_em * fontsize * dpi / 72 + line_gap = table[linegap_key] * scale + min_ascent = table[ascent_key] * scale + min_descent = -table[descent_key] * scale + break + if None in (min_ascent, min_descent): + # Fallback to font measurement. + _, h, min_descent = _get_text_metrics_with_cache( + renderer, "lp", self._fontproperties, + ismath="TeX" if self.get_usetex() else False, + dpi=dpi) + min_ascent = h - min_descent + line_gap = 0 + + # Don't increase text height too much if it's not multiple lines. + if len(lines) == 1: + line_gap = 0 + + for line in lines: clean_line, ismath = self._preprocess_math(line) if clean_line: w, h, d = _get_text_metrics_with_cache( @@ -459,25 +482,27 @@ def _get_layout(self, renderer): else: w = h = d = 0 - # For multiline text, increase the line spacing when the text - # net-height (excluding baseline) is larger than that of a "l" - # (e.g., use of superscripts), which seems what TeX does. - h = max(h, lp_h) - d = max(d, lp_d) + a = h - d - ws.append(w) - hs.append(h) + if self.get_usetex() or self._linespacing == 'normal': + # To ensure good linespacing, pretend that the ascent / descent of all + # lines is at least as large as the measured sizes. + a = max(a, min_ascent) + line_gap / 2 + d = max(d, min_descent) + line_gap / 2 + else: + # If using a fixed line spacing, then every line's spacing will be + # determined by the font metrics of the first available font. + line_height = self._linespacing * (min_ascent + min_descent) + leading = line_height - (a + d) + a += leading / 2 + d += leading / 2 # Metrics of the last line that are needed later: - baseline = (h - d) - thisy + baseline = a - thisy - if i == 0: - # position at baseline - thisy = -(h - d) - else: - # put baseline a good distance from bottom of previous line - thisy -= max(min_dy, (h - d) * self._linespacing) + thisy -= a + wads.append((w, a, d)) xs.append(thisx) # == 0. ys.append(thisy) @@ -487,15 +512,13 @@ def _get_layout(self, renderer): descent = d # Bounding box definition: + ws = [w for w, a, d in wads] width = max(ws) xmin = 0 xmax = width ymax = 0 ymin = ys[-1] - descent # baseline of last line minus its descent - # get the rotation matrix - M = Affine2D().rotate_deg(self.get_rotation()) - # now offset the individual text lines within the box malign = self._get_multialignment() if malign == 'left': @@ -508,18 +531,19 @@ def _get_layout(self, renderer): for x, y, w in zip(xs, ys, ws)] # the corners of the unrotated bounding box - corners_horiz = np.array( - [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]) - + corners_horiz = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] + size_horiz = (xmax - xmin, ymax - ymin) # now rotate the bbox - corners_rotated = M.transform(corners_horiz) + angle = self.get_rotation() + rotate = functools.partial(_rotate_point, angle) + corners_rotated = [rotate(x, y) for x, y in corners_horiz] + # compute the bounds of the rotated box - xmin = corners_rotated[:, 0].min() - xmax = corners_rotated[:, 0].max() - ymin = corners_rotated[:, 1].min() - ymax = corners_rotated[:, 1].max() - width = xmax - xmin - height = ymax - ymin + xs, ys = zip(*corners_rotated) + xmin, xmax = min(xs), max(xs) + ymin, ymax = min(ys), max(ys) + width_rot = xmax - xmin + height_rot = ymax - ymin # Now move the box to the target position offset the display # bbox by alignment @@ -528,63 +552,53 @@ def _get_layout(self, renderer): rotation_mode = self.get_rotation_mode() if rotation_mode != "anchor": - angle = self.get_rotation() if rotation_mode == 'xtick': halign = self._ha_for_angle(angle) elif rotation_mode == 'ytick': valign = self._va_for_angle(angle) # compute the text location in display coords and the offsets # necessary to align the bbox with that location - if halign == 'center': - offsetx = (xmin + xmax) / 2 - elif halign == 'right': - offsetx = xmax - else: - offsetx = xmin - - if valign == 'center': - offsety = (ymin + ymax) / 2 - elif valign == 'top': - offsety = ymax - elif valign == 'baseline': - offsety = ymin + descent - elif valign == 'center_baseline': - offsety = ymin + height - baseline / 2.0 - else: - offsety = ymin + offsetx = ( + xmin if halign == "left" else + xmax if halign == "right" else + (xmin + xmax) / 2 # halign == "center" + ) + offsety = ( + ymin if valign == "bottom" else + ymax if valign == "top" else + (ymin + ymax) / 2 if valign == "center" else + ymin + descent if valign == "baseline" else + ymin + height_rot - baseline / 2 # valign == "center_baseline" + ) else: xmin1, ymin1 = corners_horiz[0] xmax1, ymax1 = corners_horiz[2] - - if halign == 'center': - offsetx = (xmin1 + xmax1) / 2.0 - elif halign == 'right': - offsetx = xmax1 - else: - offsetx = xmin1 - - if valign == 'center': - offsety = (ymin1 + ymax1) / 2.0 - elif valign == 'top': - offsety = ymax1 - elif valign == 'baseline': - offsety = ymax1 - baseline - elif valign == 'center_baseline': - offsety = ymax1 - baseline / 2.0 - else: - offsety = ymin1 - - offsetx, offsety = M.transform((offsetx, offsety)) + offsetx = ( + xmin1 if halign == "left" else + xmax1 if halign == "right" else + (xmin1 + xmax1) / 2 # halign == "center" + ) + offsety = ( + ymin1 if valign == "bottom" else + ymax1 if valign == "top" else + (ymin1 + ymax1) / 2 if valign == "center" else + ymax1 - baseline if valign == "baseline" else + ymax1 - baseline / 2 # valign == "center_baseline" + ) + offsetx, offsety = rotate(offsetx, offsety) xmin -= offsetx ymin -= offsety - bbox = Bbox.from_bounds(xmin, ymin, width, height) + bbox_rot = Bbox.from_bounds(xmin, ymin, width_rot, height_rot) # now rotate the positions around the first (x, y) position - xys = M.transform(offset_layout) - (offsetx, offsety) + xys = [(x - offsetx, y - offsety) + for x, y in itertools.starmap(rotate, offset_layout)] + x, y = corners_rotated[0] + xy_corner = (x - offsetx, y - offsety) - return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent + return bbox_rot, list(zip(lines, wads, xys)), (xy_corner, size_horiz) def set_bbox(self, rectprops): """ @@ -656,7 +670,7 @@ def update_bbox_position_size(self, renderer): posy = float(self.convert_yunits(self._y)) posx, posy = self.get_transform().transform((posx, posy)) - x_box, y_box, w_box, h_box = _get_textbox(self, renderer) + _, _, ((x_box, y_box), (w_box, h_box)) = self._get_layout(renderer) self._bbox_patch.set_bounds(0., 0., w_box, h_box) self._bbox_patch.set_transform( Affine2D() @@ -723,11 +737,11 @@ def _get_wrap_line_width(self): # Calculate available width based on text alignment alignment = self.get_horizontalalignment() self.set_rotation_mode('anchor') - rotation = self.get_rotation() + angle = self.get_rotation() - left = self._get_dist_to_box(rotation, x0, y0, figure_box) + left = self._get_dist_to_box(angle, x0, y0, figure_box) right = self._get_dist_to_box( - (180 + rotation) % 360, x0, y0, figure_box) + (180 + angle) % 360, x0, y0, figure_box) if alignment == 'left': line_width = left @@ -836,66 +850,65 @@ def draw(self, renderer): renderer.open_group('text', self.get_gid()) - with self._cm_set(text=self._get_wrapped_text()): - bbox, info, descent = self._get_layout(renderer) - trans = self.get_transform() + bbox, info, _ = self._get_layout(renderer) + trans = self.get_transform() + + # don't use self.get_position here, which refers to text + # position in Text: + x, y = self._x, self._y + if np.ma.is_masked(x): + x = np.nan + if np.ma.is_masked(y): + y = np.nan + posx = float(self.convert_xunits(x)) + posy = float(self.convert_yunits(y)) + posx, posy = trans.transform((posx, posy)) + if np.isnan(posx) or np.isnan(posy): + return # don't throw a warning here + if not np.isfinite(posx) or not np.isfinite(posy): + _log.warning("posx and posy should be finite values") + return + canvasw, canvash = renderer.get_canvas_width_height() - # don't use self.get_position here, which refers to text - # position in Text: - x, y = self._x, self._y - if np.ma.is_masked(x): - x = np.nan - if np.ma.is_masked(y): - y = np.nan - posx = float(self.convert_xunits(x)) - posy = float(self.convert_yunits(y)) - posx, posy = trans.transform((posx, posy)) - if np.isnan(posx) or np.isnan(posy): - return # don't throw a warning here - if not np.isfinite(posx) or not np.isfinite(posy): - _log.warning("posx and posy should be finite values") - return - canvasw, canvash = renderer.get_canvas_width_height() - - # Update the location and size of the bbox - # (`.patches.FancyBboxPatch`), and draw it. - if self._bbox_patch: - self.update_bbox_position_size(renderer) - self._bbox_patch.draw(renderer) - - gc = renderer.new_gc() - gc.set_foreground(mcolors.to_rgba(self.get_color()), isRGBA=True) - gc.set_alpha(self.get_alpha()) - gc.set_url(self._url) - gc.set_antialiased(self._antialiased) - self._set_gc_clip(gc) - - angle = self.get_rotation() - - for line, wh, x, y in info: - - mtext = self if len(info) == 1 else None - x = x + posx - y = y + posy - if renderer.flipy(): - y = canvash - y - clean_line, ismath = self._preprocess_math(line) - - if self.get_path_effects(): - from matplotlib.patheffects import PathEffectRenderer - textrenderer = PathEffectRenderer( - self.get_path_effects(), renderer) - else: - textrenderer = renderer - - if self.get_usetex(): - textrenderer.draw_tex(gc, x, y, clean_line, - self._fontproperties, angle, - mtext=mtext) - else: - textrenderer.draw_text(gc, x, y, clean_line, - self._fontproperties, angle, - ismath=ismath, mtext=mtext) + # Update the location and size of the bbox + # (`.patches.FancyBboxPatch`), and draw it. + if self._bbox_patch: + self.update_bbox_position_size(renderer) + self._bbox_patch.draw(renderer) + + gc = renderer.new_gc() + gc.set_foreground(mcolors.to_rgba(self.get_color()), isRGBA=True) + gc.set_alpha(self.get_alpha()) + gc.set_url(self._url) + gc.set_antialiased(self._antialiased) + gc.set_snap(self.get_snap()) + self._set_gc_clip(gc) + + angle = self.get_rotation() + + for line, wad, (x, y) in info: + + mtext = self if len(info) == 1 else None + x = x + posx + y = y + posy + if renderer.flipy(): + y = canvash - y + clean_line, ismath = self._preprocess_math(line) + + if self.get_path_effects(): + from matplotlib.patheffects import PathEffectRenderer + textrenderer = PathEffectRenderer(self.get_path_effects(), renderer) + else: + textrenderer = renderer + + if self.get_usetex(): + textrenderer.draw_tex(gc, x, y, clean_line, + self._fontproperties, angle, + mtext=mtext) + else: + textrenderer.draw_text(gc, x, y, clean_line, + self._fontproperties, angle, + ismath=ismath, mtext=mtext) gc.restore() renderer.close_group('text') @@ -919,6 +932,12 @@ def get_fontfamily(self): """ return self._fontproperties.get_family() + def get_fontfeatures(self): + """ + Return a tuple of font feature tags to enable. + """ + return self._features + def get_fontname(self): """ Return the font name as a string. @@ -1054,13 +1073,15 @@ def get_window_extent(self, renderer=None, dpi=None): "want to call 'figure.draw_without_rendering()' first.") with cbook._setattr_cm(fig, dpi=dpi): - bbox, info, descent = self._get_layout(self._renderer) + bbox, _, _ = self._get_layout(self._renderer) x, y = self.get_unitless_position() x, y = self.get_transform().transform((x, y)) bbox = bbox.translated(x, y) return bbox def get_tightbbox(self, renderer=None): + if not self.get_visible() or self.get_text() == "": + return Bbox.null() # Exclude text at data coordinates outside the valid domain of the axes # scales (e.g., negative coordinates with a log scale). if (self.axes @@ -1139,18 +1160,26 @@ def set_multialignment(self, align): def set_linespacing(self, spacing): """ - Set the line spacing as a multiple of the font size. - - The default line spacing is 1.2. + Set the line spacing. Parameters ---------- - spacing : float (multiple of font size) + spacing : 'normal' or float, default: 'normal' + If 'normal', then the line spacing is automatically determined by font + metrics for each line individually. + + If a float, then line spacing will be fixed to this multiple of the font + size for every line. """ - _api.check_isinstance(Real, spacing=spacing) + if not cbook._str_equal(spacing, 'normal'): + _api.check_isinstance(Real, spacing=spacing) self._linespacing = spacing self.stale = True + def get_linespacing(self): + """Get the line spacing.""" + return self._linespacing + def set_fontfamily(self, fontname): """ Set the font family. Can be either a single string, or a list of @@ -1175,6 +1204,39 @@ def set_fontfamily(self, fontname): self._fontproperties.set_family(fontname) self.stale = True + def set_fontfeatures(self, features): + """ + Set the feature tags to enable on the font. + + Parameters + ---------- + features : list of str, or tuple of str, or None + A list of feature tags to be used with the associated font. These strings + are eventually passed to HarfBuzz, and so all `string formats supported by + hb_feature_from_string() + `__ + are supported. Note though that subranges are not explicitly supported and + behaviour may change in the future. + + For example, if your desired font includes Stylistic Sets which enable + various typographic alternates including one that you do not wish to use + (e.g., Contextual Ligatures), then you can pass the following to enable one + and not the other:: + + fp.set_features([ + 'ss01', # Use Stylistic Set 1. + '-clig', # But disable Contextural Ligatures. + ]) + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + """ + _api.check_isinstance((Sequence, None), features=features) + if features is not None: + features = tuple(features) + self._features = features + self.stale = True + def set_fontvariant(self, variant): """ Set the font variant. @@ -1503,6 +1565,42 @@ def _va_for_angle(self, angle): return 'baseline' if anchor_at_left else 'top' return 'top' if anchor_at_left else 'baseline' + def get_language(self): + """Return the language this Text is in.""" + return self._language + + def set_language(self, language): + """ + Set the language of the text. + + Parameters + ---------- + language : str or None + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + If None, then defaults to :rc:`text.language`. + """ + _api.check_isinstance((Sequence, str, None), language=language) + language = mpl._val_or_rc(language, 'text.language') + + if not cbook.is_scalar_or_string(language): + language = tuple(language) + for val in language: + if not isinstance(val, tuple) or len(val) != 3: + raise TypeError('language must be list of tuple, not {language!r}') + sublang, start, end = val + if not isinstance(sublang, str): + raise TypeError( + 'sub-language specification must be str, not {sublang!r}') + if not isinstance(start, int): + raise TypeError('start location must be int, not {start!r}') + if not isinstance(end, int): + raise TypeError('end location must be int, not {end!r}') + + self._language = language + self.stale = True + class OffsetFrom: """Callable helper class for working with `Annotation`.""" diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 7223693945ec..15811462224a 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -14,7 +14,7 @@ from .transforms import ( Transform, ) -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from typing import Any, Literal from .typing import ColorType, CoordsType @@ -34,7 +34,7 @@ class Text(Artist): multialignment: Literal["left", "center", "right"] | None = ..., fontproperties: str | Path | FontProperties | None = ..., rotation: float | Literal["vertical", "horizontal"] | None = ..., - linespacing: float | None = ..., + linespacing: Literal["normal"] | float | None = ..., rotation_mode: Literal["default", "anchor"] | None = ..., usetex: bool | None = ..., wrap: bool = ..., @@ -56,6 +56,7 @@ class Text(Artist): def get_color(self) -> ColorType: ... def get_fontproperties(self) -> FontProperties: ... def get_fontfamily(self) -> list[str]: ... + def get_fontfeatures(self) -> tuple[str, ...] | None: ... def get_fontname(self) -> str: ... def get_fontstyle(self) -> Literal["normal", "italic", "oblique"]: ... def get_fontsize(self) -> float | str: ... @@ -78,8 +79,10 @@ class Text(Artist): self, align: Literal["left", "center", "right"] ) -> None: ... def set_multialignment(self, align: Literal["left", "center", "right"]) -> None: ... - def set_linespacing(self, spacing: float) -> None: ... + def set_linespacing(self, spacing: Literal["normal"] | float) -> None: ... + def get_linespacing(self) -> Literal["normal"] | float: ... def set_fontfamily(self, fontname: str | Iterable[str]) -> None: ... + def set_fontfeatures(self, features: Sequence[str] | None) -> None: ... def set_fontvariant(self, variant: Literal["normal", "small-caps"]) -> None: ... def set_fontstyle( self, fontstyle: Literal["normal", "italic", "oblique"] @@ -108,6 +111,8 @@ class Text(Artist): def set_antialiased(self, antialiased: bool) -> None: ... def _ha_for_angle(self, angle: Any) -> Literal['center', 'right', 'left'] | None: ... def _va_for_angle(self, angle: Any) -> Literal['center', 'top', 'baseline'] | None: ... + def get_language(self) -> str | tuple[tuple[str, int, int], ...] | None: ... + def set_language(self, language: str | Sequence[tuple[str, int, int]] | None) -> None: ... class OffsetFrom: def __init__( diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 8deae19c42e7..d7c1cdf1622f 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -39,11 +39,9 @@ def _get_font(self, prop): def _get_hinting_flag(self): return LoadFlags.NO_HINTING - def _get_char_id(self, font, ccode): - """ - Return a unique id for the given font and character-code set. - """ - return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}") + def _get_glyph_repr(self, font, glyph): + """Return a unique id for the given font and glyph index.""" + return urllib.parse.quote(f"{font.postscript_name}-{glyph:x}") def get_text_width_height_descent(self, s, prop, ismath): fontsize = prop.get_size_in_points() @@ -69,7 +67,7 @@ def get_text_width_height_descent(self, s, prop, ismath): d /= 64.0 return w * scale, h * scale, d * scale - def get_text_path(self, prop, s, ismath=False): + def get_text_path(self, prop, s, ismath=False, *, features=None, language=None): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). @@ -82,6 +80,9 @@ def get_text_path(self, prop, s, ismath=False): The text to be converted. ismath : {False, True, "TeX"} If True, use mathtext parser. If "TeX", use tex for rendering. + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Returns ------- @@ -109,13 +110,14 @@ def get_text_path(self, prop, s, ismath=False): glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) - glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) + glyph_info, glyph_map, rects = self.get_glyphs_with_font( + font, s, features=features, language=language) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) verts, codes = [], [] - for glyph_id, xposition, yposition, scale in glyph_info: - verts1, codes1 = glyph_map[glyph_id] + for glyph_repr, xposition, yposition, scale in glyph_info: + verts1, codes1 = glyph_map[glyph_repr] verts.extend(verts1 * scale + [xposition, yposition]) codes.extend(codes1) for verts1, codes1 in rects: @@ -130,7 +132,8 @@ def get_text_path(self, prop, s, ismath=False): return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, - return_new_glyphs_only=False): + return_new_glyphs_only=False, *, features=None, + language=None): """ Convert string *s* to vertices and codes using the provided ttf font. """ @@ -144,20 +147,21 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, glyph_map_new = glyph_map xpositions = [] - glyph_ids = [] - for item in _text_helpers.layout(s, font): - char_id = self._get_char_id(item.ft_object, ord(item.char)) - glyph_ids.append(char_id) + ypositions = [] + glyph_reprs = [] + for item in _text_helpers.layout(s, font, features=features, language=language): + glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) + glyph_reprs.append(glyph_repr) xpositions.append(item.x) - if char_id not in glyph_map: - glyph_map_new[char_id] = item.ft_object.get_path() + ypositions.append(item.y) + if glyph_repr not in glyph_map: + glyph_map_new[glyph_repr] = item.ft_object.get_path() - ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, @@ -182,20 +186,20 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, xpositions = [] ypositions = [] - glyph_ids = [] + glyph_reprs = [] sizes = [] - for font, fontsize, ccode, ox, oy in glyphs: - char_id = self._get_char_id(font, ccode) - if char_id not in glyph_map: + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + glyph_repr = self._get_glyph_repr(font, glyph_index) + if glyph_repr not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LoadFlags.NO_HINTING) - glyph_map_new[char_id] = font.get_path() + font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) + glyph_map_new[glyph_repr] = font.get_path() xpositions.append(ox) ypositions.append(oy) - glyph_ids.append(char_id) + glyph_reprs.append(glyph_repr) size = fontsize / self.FONT_SCALE sizes.append(size) @@ -208,7 +212,7 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, Path.CLOSEPOLY] myrects.append((vert1, code1)) - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_glyphs_tex(self, prop, s, glyph_map=None, @@ -228,23 +232,22 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, else: glyph_map_new = glyph_map - glyph_ids, xpositions, ypositions, sizes = [], [], [], [] + glyph_reprs, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. - t1_encodings = {} for text in page.text: font = get_font(text.font.resolve_path()) if text.font.subfont: raise NotImplementedError("Indexing TTC fonts is not supported yet") - char_id = self._get_char_id(font, text.glyph) - if char_id not in glyph_map: + glyph_repr = self._get_glyph_repr(font, text.index) + if glyph_repr not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_glyph(text.index, flags=LoadFlags.TARGET_LIGHT) - glyph_map_new[char_id] = font.get_path() + glyph_map_new[glyph_repr] = font.get_path() - glyph_ids.append(char_id) + glyph_reprs.append(glyph_repr) xpositions.append(text.x) ypositions.append(text.y) sizes.append(text.font_size / self.FONT_SCALE) @@ -259,7 +262,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, Path.CLOSEPOLY] myrects.append((vert1, code1)) - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, myrects) diff --git a/lib/matplotlib/textpath.pyi b/lib/matplotlib/textpath.pyi index 34d4e92ac47e..07f81598aa75 100644 --- a/lib/matplotlib/textpath.pyi +++ b/lib/matplotlib/textpath.pyi @@ -16,7 +16,13 @@ class TextToPath: self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"] ) -> tuple[float, float, float]: ... def get_text_path( - self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ... + self, + prop: FontProperties, + s: str, + ismath: bool | Literal["TeX"] = ..., + *, + features: tuple[str] | None = ..., + language: str | list[tuple[str, int, int]] | None = ..., ) -> list[np.ndarray]: ... def get_glyphs_with_font( self, @@ -24,6 +30,9 @@ class TextToPath: s: str, glyph_map: dict[str, tuple[np.ndarray, np.ndarray]] | None = ..., return_new_glyphs_only: bool = ..., + *, + features: tuple[str] | None = ..., + language: str | list[tuple[str, int, int]] | None = ..., ) -> tuple[ list[tuple[str, float, float, float]], dict[str, tuple[np.ndarray, np.ndarray]], diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 4e77020d3f8e..a279de0dfd8b 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -841,7 +841,7 @@ def from_extents(*args, minpos=None): set. This is useful when dealing with logarithmic scales and other scales where negative bounds result in floating point errors. """ - bbox = Bbox(np.reshape(args, (2, 2))) + bbox = Bbox(np.asarray(args, dtype=float).reshape((2, 2))) if minpos is not None: bbox._minpos[:] = minpos return bbox diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 87016984da12..e194874e13ff 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -454,6 +454,8 @@ "text.hinting", "text.hinting_factor", "text.kerning_factor", + "text.language", + "text.latex.engine", "text.latex.preamble", "text.parse_math", "text.usetex", diff --git a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index 7f54466a3cce..5a6a229f3c59 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -1,6 +1,7 @@ from itertools import product import io import platform +import sys import matplotlib as mpl import matplotlib.pyplot as plt @@ -61,16 +62,14 @@ def test_divider_append_axes(): assert bboxes["top"].x1 == bboxes["main"].x1 == bboxes["bottom"].x1 -# Update style when regenerating the test image -@image_comparison(['twin_axes_empty_and_removed.png'], tol=1, - style=('classic', '_classic_test_patch')) +@image_comparison(['twin_axes_empty_and_removed.png'], style='mpl20') def test_twin_axes_empty_and_removed(): # Purely cosmetic font changes (avoid overlap) - mpl.rcParams.update( - {"font.size": 8, "xtick.labelsize": 8, "ytick.labelsize": 8}) + mpl.rcParams.update({"font.size": 8, "xtick.labelsize": 8, "ytick.labelsize": 8}) generators = ["twinx", "twiny", "twin"] modifiers = ["", "host invisible", "twin removed", "twin invisible", "twin removed\nhost invisible"] + plt.figure(figsize=(8, 6)) # Unmodified host subplot at the beginning for reference h = host_subplot(len(modifiers)+1, len(generators), 2) h.text(0.5, 0.5, "host_subplot", @@ -343,10 +342,8 @@ def test_fill_facecolor(): mark_inset(ax[3], axins, loc1=2, loc2=4, fc="g", ec="0.5", fill=False) -# Update style when regenerating the test image -@image_comparison(['zoomed_axes.png', 'inverted_zoomed_axes.png'], - style=('classic', '_classic_test_patch'), - tol=0 if platform.machine() == 'x86_64' else 0.02) +@image_comparison(['zoomed_axes.png', 'inverted_zoomed_axes.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.03) def test_zooming_with_inverted_axes(): fig, ax = plt.subplots() ax.plot([1, 2, 3], [1, 2, 3]) @@ -361,10 +358,8 @@ def test_zooming_with_inverted_axes(): inset_ax.axis([1.4, 1.1, 1.4, 1.1]) -# Update style when regenerating the test image -@image_comparison(['anchored_direction_arrows.png'], - tol=0 if platform.machine() == 'x86_64' else 0.01, - style=('classic', '_classic_test_patch')) +@image_comparison(['anchored_direction_arrows.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.006) def test_anchored_direction_arrows(): fig, ax = plt.subplots() ax.imshow(np.zeros((10, 10)), interpolation='nearest') @@ -373,9 +368,8 @@ def test_anchored_direction_arrows(): ax.add_artist(simple_arrow) -# Update style when regenerating the test image -@image_comparison(['anchored_direction_arrows_many_args.png'], - style=('classic', '_classic_test_patch')) +@image_comparison(['anchored_direction_arrows_many_args.png'], style='mpl20', + tol=0.002 if sys.platform == 'win32' else 0) def test_anchored_direction_arrows_many_args(): fig, ax = plt.subplots() ax.imshow(np.ones((10, 10))) @@ -562,15 +556,6 @@ def test_anchored_artists(): box.drawing_area.add_artist(el) ax.add_artist(box) - # This block used to test the AnchoredEllipse class, but that was removed. The block - # remains, though it duplicates the above ellipse, so that the test image doesn't - # need to be regenerated. - box = AnchoredAuxTransformBox(ax.transData, loc='lower left', frameon=True, - pad=0.5, borderpad=0.4) - el = Ellipse((0, 0), width=0.1, height=0.25, angle=-60) - box.drawing_area.add_artist(el) - ax.add_artist(box) - asb = AnchoredSizeBar(ax.transData, 0.2, r"0.2 units", loc='lower right', pad=0.3, borderpad=0.4, sep=4, fill_bar=True, frameon=False, label_top=True, prop={'size': 20}, diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index f366b9e96537..8c67b18c0349 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -24,12 +24,8 @@ def test_ticks(): ax.add_artist(ticks_out) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['axis_artist_labelbase.png'], style='default', tol=0.02) +@image_comparison(['axis_artist_labelbase.png'], style='default') def test_labelbase(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.plot([0.5], [0.5], "o") @@ -42,12 +38,8 @@ def test_labelbase(): ax.add_artist(label) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['axis_artist_ticklabels.png'], style='default', tol=0.03) +@image_comparison(['axis_artist_ticklabels.png'], style='default') def test_ticklabels(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.xaxis.set_visible(False) @@ -78,12 +70,8 @@ def test_ticklabels(): ax.set_ylim(0, 1) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['axis_artist.png'], style='default', tol=0.03) +@image_comparison(['axis_artist.png'], style='default') def test_axis_artist(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.xaxis.set_visible(False) @@ -92,7 +80,9 @@ def test_axis_artist(): for loc in ('left', 'right', 'bottom'): helper = AxisArtistHelperRectlinear.Fixed(ax, loc=loc) axisline = AxisArtist(ax, helper, offset=None, axis_direction=loc) - axisline.major_ticks.set_tick_direction("in") + axisline.major_ticks.set_tick_direction({ + "left": "in", "right": "out", "bottom": "inout", + }[loc]) ax.add_artist(axisline) # Settings for bottom AxisArtist. diff --git a/lib/mpl_toolkits/axisartist/tests/test_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py index 10c50c271ef2..a13432182c58 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -7,13 +7,8 @@ from mpl_toolkits.axisartist import Axes, SubplotHost -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['SubplotZero.png'], style='default', tol=0.02) +@image_comparison(['SubplotZero.png'], style='mpl20') def test_SubplotZero(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({ - "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) - fig = plt.figure() ax = SubplotZero(fig, 1, 1, 1) @@ -30,13 +25,8 @@ def test_SubplotZero(): ax.set_ylabel("Test") -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['Subplot.png'], style='default', tol=0.02) +@image_comparison(['Subplot.png'], style='mpl20') def test_Subplot(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({ - "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) - fig = plt.figure() ax = Subplot(fig, 1, 1, 1) @@ -46,8 +36,8 @@ def test_Subplot(): ax.plot(xx, np.sin(xx)) ax.set_ylabel("Test") - ax.axis["top"].major_ticks.set_tick_out(True) - ax.axis["bottom"].major_ticks.set_tick_out(True) + ax.axis["left"].major_ticks.set_tick_out(False) + ax.axis["right"].major_ticks.set_tick_out(False) ax.axis["bottom"].set_label("Tk0") @@ -62,9 +52,8 @@ def test_Axes(): @image_comparison(['ParasiteAxesAuxTrans_meshplot.png'], - remove_text=True, style='default', tol=0.075) + remove_text=True, style='mpl20', tol=0.075) def test_ParasiteAxesAuxTrans(): - # Remove this line when this test image is regenerated. plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) data = np.ones((6, 6)) data[2, 2] = 2 @@ -142,11 +131,8 @@ def test_axisline_style_tight(): ax.axis[direction].set_visible(False) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['subplotzero_ylabel.png'], style='mpl20', tol=0.02) +@image_comparison(['subplotzero_ylabel.png'], style='mpl20') def test_subplotzero_ylabel(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure() ax = fig.add_subplot(111, axes_class=SubplotZero) diff --git a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index 98d49dc0cf37..5575a48d499a 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -19,12 +19,8 @@ def test_subplot(): fig.add_subplot(ax) -# Rather high tolerance to allow ongoing work with floating axes internals; -# remove when image is regenerated. -@image_comparison(['curvelinear3.png'], style='default', tol=5) +@image_comparison(['curvelinear3.png'], style='mpl20') def test_curvelinear3(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) fig = plt.figure(figsize=(5, 5)) tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + @@ -67,14 +63,8 @@ def test_curvelinear3(): l.set_clip_path(ax1.patch) -# Rather high tolerance to allow ongoing work with floating axes internals; -# remove when image is regenerated. -@image_comparison(['curvelinear4.png'], style='default', tol=0.9) +@image_comparison(['curvelinear4.png'], style='mpl20', tol=0.04) def test_curvelinear4(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({ - "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) - fig = plt.figure(figsize=(5, 5)) tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index e59e31fe3c88..ab1eedd9b797 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -1,3 +1,5 @@ +import platform + import numpy as np import matplotlib.pyplot as plt @@ -15,10 +17,10 @@ GridHelperCurveLinear -@image_comparison(['custom_transform.png'], style='default', tol=0.2) +@image_comparison(['custom_transform.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.04) def test_custom_transform(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) + plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "inout"}) class MyTransform(Transform): input_dims = output_dims = 2 @@ -79,11 +81,9 @@ def inverted(self): ax1.grid(True) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['polar_box.png'], style='default', tol=0.09) +@image_comparison(['polar_box.png'], style='mpl20', tol=0.04) def test_polar_box(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({"xtick.direction": "in", "ytick.direction": "in"}) + plt.rcParams.update({"xtick.direction": "inout", "ytick.direction": "out"}) fig = plt.figure(figsize=(5, 5)) # PolarAxes.PolarTransform takes radian. However, we want our coordinate @@ -141,13 +141,8 @@ def test_polar_box(): ax1.grid(True) -# Remove tol & kerning_factor when this test image is regenerated. -@image_comparison(['axis_direction.png'], style='default', tol=0.15) +@image_comparison(['axis_direction.png'], style='mpl20', tol=0.04) def test_axis_direction(): - # Remove this line when this test image is regenerated. - plt.rcParams.update({ - "text.kerning_factor": 6, "xtick.direction": "in", "ytick.direction": "in"}) - fig = plt.figure(figsize=(5, 5)) # PolarAxes.PolarTransform takes radian. However, we want our coordinate diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 8d2441393dde..ac0168ce775e 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -117,7 +117,7 @@ def test_axes3d_repr(): @mpl3d_image_comparison(['axes3d_primary_views.png'], style='mpl20', - tol=0.05 if sys.platform == "darwin" else 0) + tol=0.045 if sys.platform == 'darwin' else 0) def test_axes3d_primary_views(): # (elev, azim, roll) views = [(90, -90, 0), # XY @@ -647,10 +647,8 @@ def test_surface3d(): fig.colorbar(surf, shrink=0.5, aspect=5) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@image_comparison(['surface3d_label_offset_tick_position.png'], style='mpl20', tol=0.07) +@image_comparison(['surface3d_label_offset_tick_position.png'], style='mpl20') def test_surface3d_label_offset_tick_position(): - plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated ax = plt.figure().add_subplot(projection="3d") x, y = np.mgrid[0:6 * np.pi:0.25, 0:4 * np.pi:0.25] @@ -744,8 +742,7 @@ def test_surface3d_masked_strides(): ax.view_init(60, -45, 0) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@mpl3d_image_comparison(['text3d.png'], remove_text=False, style='mpl20', tol=0.1) +@mpl3d_image_comparison(['text3d.png'], remove_text=False, style='mpl20') def test_text3d(): fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -1124,9 +1121,7 @@ def test_poly3dCollection_autoscaling(): assert np.allclose(ax.get_zlim3d(), (-0.0833333333333333, 4.083333333333333)) -# TODO: tighten tolerance after baseline image is regenerated for text overhaul -@mpl3d_image_comparison(['axes3d_labelpad.png'], - remove_text=False, style='mpl20', tol=0.06) +@mpl3d_image_comparison(['axes3d_labelpad.png'], remove_text=False, style='mpl20') def test_axes3d_labelpad(): fig = plt.figure() ax = fig.add_axes(Axes3D(fig)) @@ -1501,8 +1496,8 @@ def test_alpha(self): assert voxels[coord], "faces returned for absent voxel" assert isinstance(poly, art3d.Poly3DCollection) - @mpl3d_image_comparison(['voxels-xyz.png'], - tol=0.01, remove_text=False, style='mpl20') + @mpl3d_image_comparison(['voxels-xyz.png'], remove_text=False, style='mpl20', + tol=0.002 if sys.platform == 'win32' else 0) def test_xyz(self): fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) @@ -1715,7 +1710,7 @@ def test_errorbar3d_errorevery(): @mpl3d_image_comparison(['errorbar3d.png'], style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.02) + tol=0.015 if sys.platform == 'darwin' else 0) def test_errorbar3d(): """Tests limits, color styling, and legend for 3D errorbars.""" fig = plt.figure() @@ -1731,9 +1726,9 @@ def test_errorbar3d(): ax.legend() -@image_comparison(['stem3d.png'], style='mpl20', tol=0.009) +@image_comparison(['stem3d.png'], style='mpl20', + tol=0 if platform.machine() == 'x86_64' else 0.008) def test_stem3d(): - plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated fig, axs = plt.subplots(2, 3, figsize=(8, 6), constrained_layout=True, subplot_kw={'projection': '3d'}) @@ -2878,7 +2873,7 @@ def _make_triangulation_data(): @mpl3d_image_comparison(['scale3d_artists_log.png'], style='mpl20', - remove_text=False, tol=0.03) + remove_text=False, tol=0.016) def test_scale3d_artists_log(): """Test all 3D artist types with log scale.""" fig = plt.figure(figsize=(16, 12)) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py index 9ca048e18ba9..a46c958222d8 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py @@ -1,4 +1,4 @@ -import platform +import sys import numpy as np @@ -28,7 +28,7 @@ def test_legend_bar(): @image_comparison(['fancy.png'], remove_text=True, style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.011) + tol=0.01 if sys.platform == 'darwin' else 0) def test_fancy(): fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) ax.plot(np.arange(10), np.full(10, 5), np.full(10, 5), 'o--', label='line') diff --git a/meson.build b/meson.build index 47244656705f..820335e2c9d8 100644 --- a/meson.build +++ b/meson.build @@ -7,18 +7,24 @@ project( '-m', 'setuptools_scm', check: true).stdout().strip(), # qt_editor backend is MIT # ResizeObserver at end of lib/matplotlib/backends/web_backend/js/mpl.js is CC0 - # Carlogo, STIX and Computer Modern is OFL + # Carlogo, STIX, Computer Modern, and Last Resort are OFL # DejaVu is Bitstream Vera and Public Domain license: 'PSF-2.0 AND MIT AND CC0-1.0 AND OFL-1.1 AND Bitstream-Vera AND Public-Domain', license_files: [ 'LICENSE/LICENSE', + 'extern/agg24-svn/src/copying', 'LICENSE/LICENSE_AMSFONTS', 'LICENSE/LICENSE_BAKOMA', 'LICENSE/LICENSE_CARLOGO', 'LICENSE/LICENSE_COLORBREWER', 'LICENSE/LICENSE_COURIERTEN', + 'LICENSE/LICENSE_FREETYPE', + 'LICENSE/LICENSE_HARFBUZZ', 'LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER', + 'LICENSE/LICENSE_LAST_RESORT_FONT', + 'LICENSE/LICENSE_LIBRAQM', 'LICENSE/LICENSE_QT4_EDITOR', + 'LICENSE/LICENSE_SHEENBIDI', 'LICENSE/LICENSE_SOLARIZED', 'LICENSE/LICENSE_STIX', 'LICENSE/LICENSE_YORICK', diff --git a/meson.options b/meson.options index d21cbedb9bb9..7e03ff405f85 100644 --- a/meson.options +++ b/meson.options @@ -7,6 +7,8 @@ # FreeType on AIX. option('system-freetype', type: 'boolean', value: false, description: 'Build against system version of FreeType') +option('system-libraqm', type: 'boolean', value: false, + description: 'Build against system version of libraqm') option('system-qhull', type: 'boolean', value: false, description: 'Build against system version of Qhull') diff --git a/pyproject.toml b/pyproject.toml index 6275d0e30531..2d1308b63347 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ build-backend = "mesonpy" requires = [ # meson-python 0.17.x breaks symlinks in sdists. You can remove this pin if # you really need it and aren't using an sdist. - "meson-python>=0.13.1,!=0.17.*", + "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", "setuptools_scm>=7,<10", ] @@ -417,7 +417,10 @@ test-command = [ cp -a {package}/lib/${subdir}/tests/baseline_images $basedir/${subdir}/tests/ done""", # Test installed, not repository, copy as we aren't using an editable install. - "pytest -p no:cacheprovider --pyargs matplotlib mpl_toolkits.axes_grid1 mpl_toolkits.axisartist mpl_toolkits.mplot3d", + """\ + pytest -p no:cacheprovider --pyargs \ + matplotlib mpl_toolkits.axes_grid1 mpl_toolkits.axisartist mpl_toolkits.mplot3d \ + -k 'not test_complex_shaping'""", ] [tool.cibuildwheel.pyodide.environment] # Exceptions are needed for pybind11: diff --git a/src/_path.h b/src/_path.h index 226d60231682..fc11a00aa09f 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1055,38 +1055,25 @@ void quad2cubic(double x0, double y0, void __add_number(double val, char format_code, int precision, std::string& buffer) { - if (precision == -1) { - // Special-case for compat with old ttconv code, which *truncated* - // values with a cast to int instead of rounding them as printf - // would do. The only point where non-integer values arise is from - // quad2cubic conversion (as we already perform a first truncation - // on Python's side), which can introduce additional floating point - // error (by adding 2/3 delta-x and then 1/3 delta-x), so compensate by - // first rounding to the closest 1/3 and then truncating. - char str[255]; - PyOS_snprintf(str, 255, "%d", (int)(round(val * 3)) / 3); - buffer += str; - } else { - char *str = PyOS_double_to_string( - val, format_code, precision, Py_DTSF_ADD_DOT_0, nullptr); - // Delete trailing zeros and decimal point - char *c = str + strlen(str) - 1; // Start at last character. - // Rewind through all the zeros and, if present, the trailing decimal - // point. Py_DTSF_ADD_DOT_0 ensures we won't go past the start of str. - while (*c == '0') { - --c; - } - if (*c == '.') { - --c; - } - try { - buffer.append(str, c + 1); - } catch (std::bad_alloc& e) { - PyMem_Free(str); - throw e; - } + char *str = PyOS_double_to_string( + val, format_code, precision, Py_DTSF_ADD_DOT_0, nullptr); + // Delete trailing zeros and decimal point + char *c = str + strlen(str) - 1; // Start at last character. + // Rewind through all the zeros and, if present, the trailing decimal + // point. Py_DTSF_ADD_DOT_0 ensures we won't go past the start of str. + while (*c == '0') { + --c; + } + if (*c == '.') { + --c; + } + try { + buffer.append(str, c + 1); + } catch (std::bad_alloc& e) { PyMem_Free(str); + throw e; } + PyMem_Free(str); } diff --git a/src/ft2font.cpp b/src/ft2font.cpp index da1bd19dca57..dc9397dd75f0 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -6,8 +6,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -207,38 +207,48 @@ FT2Font::get_path(std::vector &vertices, std::vector &cod codes.push_back(CLOSEPOLY); } -FT2Font::FT2Font(FT_Open_Args &open_args, - long hinting_factor_, - std::vector &fallback_list, - FT2Font::WarnFunc warn, bool warn_if_used) - : ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr), +FT2Font::FT2Font(long hinting_factor_, std::vector &fallback_list, + bool warn_if_used) + : warn_if_used(warn_if_used), image({1, 1}), face(nullptr), fallbacks(fallback_list), hinting_factor(hinting_factor_), // set default kerning factor to 0, i.e., no kerning manipulation kerning_factor(0) { clear(); - FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face); +} + +FT2Font::~FT2Font() +{ + close(); +} + +void FT2Font::open(FT_Open_Args &open_args, FT_Long face_index) +{ + FT_CHECK(FT_Open_Face, _ft2Library, &open_args, face_index, &face); if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } - try { - set_size(12., 72.); // Set a default fontsize 12 pt at 72dpi. - } catch (...) { - FT_Done_Face(face); - throw; - } - // Set fallbacks - std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); + + // This allows us to get back to our data if we need it, though it makes a pointer + // loop, so don't set a free-function for it. + face->generic.data = this; + face->generic.finalizer = nullptr; } -FT2Font::~FT2Font() +void FT2Font::close() { + // This should be idempotent, in case a user manually calls close before the + // destructor does. Note for example, that PyFT2Font _does_ call this before the + // base destructor to ensure internal pointers are cleared early enough. + for (auto & glyph : glyphs) { FT_Done_Glyph(glyph); } + glyphs.clear(); if (face) { FT_Done_Face(face); + face = nullptr; } } @@ -253,7 +263,6 @@ void FT2Font::clear() } glyphs.clear(); - glyph_to_font.clear(); char_to_font.clear(); for (auto & fallback : fallbacks) { @@ -274,6 +283,17 @@ void FT2Font::set_size(double ptsize, double dpi) } } +void FT2Font::_set_transform( + std::array, 2> matrix, std::array delta) +{ + FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]}; + FT_Vector d = {delta[0], delta[1]}; + FT_Set_Transform(face, &m, &d); + for (auto & fallback : fallbacks) { + fallback->_set_transform(matrix, delta); + } +} + void FT2Font::set_charmap(int i) { if (i >= face->num_charmaps) { @@ -287,35 +307,13 @@ void FT2Font::select_charmap(unsigned long i) FT_CHECK(FT_Select_Charmap, face, (FT_Encoding)i); } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, - bool fallback = false) -{ - if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && - glyph_to_font.find(right) != glyph_to_font.end()) { - FT2Font *left_ft_object = glyph_to_font[left]; - FT2Font *right_ft_object = glyph_to_font[right]; - if (left_ft_object != right_ft_object) { - // we do not know how to do kerning between different fonts - return 0; - } - // if left_ft_object is the same as right_ft_object, - // do the exact same thing which set_text does. - return right_ft_object->get_kerning(left, right, mode, false); - } - else - { - FT_Vector delta; - return get_kerning(left, right, mode, delta); - } -} - -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, - FT_Vector &delta) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode) { if (!FT_HAS_KERNING(face)) { return 0; } + FT_Vector delta; if (!FT_Get_Kerning(face, left, right, mode, &delta)) { return (int)(delta.x) / (hinting_factor << kerning_factor); } else { @@ -331,8 +329,145 @@ void FT2Font::set_kerning_factor(int factor) } } +std::vector FT2Font::layout( + std::u32string_view text, FT_Int32 flags, + std::optional> features, LanguageType languages, + std::set& glyph_seen_fonts) +{ + clear(); + + auto rq = raqm_create(); + if (!rq) { + throw std::runtime_error("failed to compute text layout"); + } + [[maybe_unused]] auto const& rq_cleanup = + std::unique_ptr, decltype(&raqm_destroy)>( + rq, raqm_destroy); + + if (!raqm_set_text(rq, reinterpret_cast(text.data()), + text.size())) + { + throw std::runtime_error("failed to set text for layout"); + } + if (!raqm_set_freetype_face(rq, face)) { + throw std::runtime_error("failed to set text face for layout"); + } + if (!raqm_set_freetype_load_flags(rq, flags)) { + throw std::runtime_error("failed to set text flags for layout"); + } + if (features) { + for (auto const& feature : *features) { + if (!raqm_add_font_feature(rq, feature.c_str(), feature.size())) { + throw std::runtime_error("failed to set font feature {}"_s.format(feature)); + } + } + } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language between {} and {} characters "_s + "to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } + if (!raqm_layout(rq)) { + throw std::runtime_error("failed to layout text"); + } + + std::vector> face_substitutions; + glyph_seen_fonts.insert(face->family_name); + + // Attempt to use fallback fonts if necessary. + for (auto const& fallback : fallbacks) { + size_t num_glyphs = 0; + auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); + bool new_fallback_used = false; + + // Sort clusters (n.b. std::map is ordered), as RTL text will be returned in + // display, not source, order. + std::map cluster_missing; + for (size_t i = 0; i < num_glyphs; i++) { + auto const& rglyph = rq_glyphs[i]; + + // Sometimes multiple glyphs are necessary for a single cluster; if any are + // not found, we want to "poison" the whole set and keep them missing. + cluster_missing[rglyph.cluster] |= (rglyph.index == 0); + } + + for (auto it = cluster_missing.cbegin(); it != cluster_missing.cend(); ) { + auto [cluster, missing] = *it; + ++it; // Early change so we can access the next cluster below. + if (missing) { + auto next = (it != cluster_missing.cend()) ? it->first : text.size(); + for (auto i = cluster; i < next; i++) { + face_substitutions.emplace_back(i, fallback->face); + } + new_fallback_used = true; + } + } + + if (!new_fallback_used) { + // If we never used a fallback, then we're good to go with the existing + // layout we have already made. + break; + } + + // If a fallback was used, then re-attempt the layout with the new fonts. + if (!fallback->warn_if_used) { + glyph_seen_fonts.insert(fallback->face->family_name); + } + + raqm_clear_contents(rq); + if (!raqm_set_text(rq, + reinterpret_cast(text.data()), + text.size())) + { + throw std::runtime_error("failed to set text for layout"); + } + if (!raqm_set_freetype_face(rq, face)) { + throw std::runtime_error("failed to set text face for layout"); + } + for (auto [cluster, fallback] : face_substitutions) { + raqm_set_freetype_face_range(rq, fallback, cluster, 1); + } + if (!raqm_set_freetype_load_flags(rq, flags)) { + throw std::runtime_error("failed to set text flags for layout"); + } + if (features) { + for (auto const& feature : *features) { + if (!raqm_add_font_feature(rq, feature.c_str(), feature.size())) { + throw std::runtime_error( + "failed to set font feature {}"_s.format(feature)); + } + } + } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language between {} and {} characters "_s + "to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } + if (!raqm_layout(rq)) { + throw std::runtime_error("failed to layout text"); + } + } + + size_t num_glyphs = 0; + auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); + + return std::vector(rq_glyphs, rq_glyphs + num_glyphs); +} + void FT2Font::set_text( - std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, + std::optional> features, LanguageType languages, + std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -347,55 +482,44 @@ void FT2Font::set_text( matrix.yx = (FT_Fixed)sinangle; matrix.yy = (FT_Fixed)cosangle; - clear(); + std::set glyph_seen_fonts; + auto rq_glyphs = layout(text, flags, features, languages, glyph_seen_fonts); bbox.xMin = bbox.yMin = 32000; bbox.xMax = bbox.yMax = -32000; - FT_UInt previous = 0; - FT2Font *previous_ft_object = nullptr; - - for (auto codepoint : text) { - FT_UInt glyph_index = 0; - FT_BBox glyph_bbox; - FT_Pos last_advance; - - FT_Error charcode_error, glyph_error; - std::set glyph_seen_fonts; - FT2Font *ft_object_with_glyph = this; - bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, glyph_to_font, codepoint, flags, - charcode_error, glyph_error, glyph_seen_fonts, false); - if (!was_found) { - ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); - // render missing glyph tofu - // come back to top-most font - ft_object_with_glyph = this; - char_to_font[codepoint] = ft_object_with_glyph; - glyph_to_font[glyph_index] = ft_object_with_glyph; - ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); - } else if (ft_object_with_glyph->warn_if_used) { - ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); + for (auto const& rglyph : rq_glyphs) { + // Warn for missing glyphs. + if (rglyph.index == 0) { + ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts); + continue; } - - // retrieve kerning distance and move pen position - if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same - ft_object_with_glyph->has_kerning() && // if the font knows how to kern - previous && glyph_index // and we really have 2 glyphs - ) { - FT_Vector delta; - pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT, delta); + FT2Font *wrapped_font = static_cast(rglyph.ftface->generic.data); + if (wrapped_font->warn_if_used) { + ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts); } // extract glyph image and store it in our table - FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1]; + FT_Error error; + error = FT_Load_Glyph(rglyph.ftface, rglyph.index, flags); + if (error) { + throw std::runtime_error("failed to load glyph"); + } + FT_Glyph thisGlyph; + error = FT_Get_Glyph(rglyph.ftface->glyph, &thisGlyph); + if (error) { + throw std::runtime_error("failed to get glyph"); + } + + pen.x += rglyph.x_offset; + pen.y += rglyph.y_offset; - last_advance = ft_object_with_glyph->get_face()->glyph->advance.x; FT_Glyph_Transform(thisGlyph, nullptr, &pen); FT_Glyph_Transform(thisGlyph, &matrix, nullptr); xys.push_back(pen.x); xys.push_back(pen.y); + FT_BBox glyph_bbox; FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox); bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin); @@ -403,11 +527,14 @@ void FT2Font::set_text( bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin); bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax); - pen.x += last_advance; - - previous = glyph_index; - previous_ft_object = ft_object_with_glyph; + if ((flags & FT_LOAD_NO_HINTING) != 0) { + pen.x += rglyph.x_advance - rglyph.x_offset; + } else { + pen.x += hinting_factor * rglyph.x_advance - rglyph.x_offset; + } + pen.y += rglyph.y_advance - rglyph.y_offset; + glyphs.push_back(thisGlyph); } FT_Vector_Transform(&pen, &matrix); @@ -434,9 +561,9 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool FT_Error charcode_error, glyph_error; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, - glyphs, char_to_font, glyph_to_font, + glyphs, char_to_font, charcode, flags, charcode_error, glyph_error, - glyph_seen_fonts, true); + glyph_seen_fonts); if (!was_found) { ft_glyph_warn(charcode, glyph_seen_fonts); if (charcode_error) { @@ -493,20 +620,18 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_UInt &final_glyph_index, std::vector &parent_glyphs, std::unordered_map &parent_char_to_font, - std::unordered_map &parent_glyph_to_font, long charcode, FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, - std::set &glyph_seen_fonts, - bool override = false) + std::set &glyph_seen_fonts) { FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); if (!warn_if_used) { glyph_seen_fonts.insert(face->family_name); } - if (glyph_index || override) { + if (glyph_index) { charcode_error = FT_Load_Glyph(face, glyph_index, flags); if (charcode_error) { return false; @@ -523,7 +648,6 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, // need to store this for anytime a character is loaded from a parent // FT2Font object or to generate a mapping of individual characters to fonts ft_object_with_glyph = this; - parent_glyph_to_font[final_glyph_index] = this; parent_char_to_font[charcode] = this; parent_glyphs.push_back(thisGlyph); return true; @@ -532,8 +656,8 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, for (auto & fallback : fallbacks) { bool was_found = fallback->load_char_with_fallback( ft_object_with_glyph, final_glyph_index, parent_glyphs, - parent_char_to_font, parent_glyph_to_font, charcode, flags, - charcode_error, glyph_error, glyph_seen_fonts, override); + parent_char_to_font, charcode, flags, + charcode_error, glyph_error, glyph_seen_fonts); if (was_found) { return true; } @@ -542,21 +666,6 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, } } -void FT2Font::load_glyph(FT_UInt glyph_index, - FT_Int32 flags, - FT2Font *&ft_object, - bool fallback = false) -{ - // cache is only for parent FT2Font - if (fallback && glyph_to_font.find(glyph_index) != glyph_to_font.end()) { - ft_object = glyph_to_font[glyph_index]; - } else { - ft_object = this; - } - - ft_object->load_glyph(glyph_index, flags); -} - void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) { FT_CHECK(FT_Load_Glyph, face, glyph_index, flags); @@ -581,10 +690,9 @@ FT_UInt FT2Font::get_char_index(FT_ULong charcode, bool fallback = false) return FT_Get_Char_Index(ft_object->get_face(), charcode); } -void FT2Font::get_width_height(long *width, long *height) +std::tuple FT2Font::get_width_height() { - *width = advance; - *height = bbox.yMax - bbox.yMin; + return {advance, bbox.yMax - bbox.yMin}; } long FT2Font::get_descent() @@ -592,10 +700,9 @@ long FT2Font::get_descent() return -bbox.yMin; } -void FT2Font::get_bitmap_offset(long *x, long *y) +std::tuple FT2Font::get_bitmap_offset() { - *x = bbox.xMin; - *y = 0; + return {bbox.xMin, 0}; } void FT2Font::draw_glyphs_to_bitmap(bool antialiased) @@ -644,15 +751,11 @@ void FT2Font::draw_glyph_to_bitmap( draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, - bool fallback = false) +std::string FT2Font::get_glyph_name(unsigned int glyph_number) { - if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { - // cache is only for parent FT2Font - FT2Font *ft_object = glyph_to_font[glyph_number]; - ft_object->get_glyph_name(glyph_number, buffer, false); - return; - } + std::string buffer; + buffer.resize(128); + if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ @@ -669,6 +772,8 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, buffer.resize(len); } } + + return buffer; } long FT2Font::get_name_index(char *name) diff --git a/src/ft2font.h b/src/ft2font.h index 6676a7dd4818..3facec0fb244 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -9,14 +9,17 @@ #include #include +#include #include #include #include +#include #include #include extern "C" { #include +#include FT_BITMAP_H #include FT_FREETYPE_H #include FT_GLYPH_H #include FT_OUTLINE_H @@ -25,6 +28,8 @@ extern "C" { #include FT_TRUETYPE_TABLES_H } +#include + namespace py = pybind11; // By definition, FT_FIXED as 2 16bit values stored in a single long. @@ -96,44 +101,49 @@ extern FT_Library _ft2Library; class FT2Font { - typedef void (*WarnFunc)(FT_ULong charcode, std::set family_names); - public: - FT2Font(FT_Open_Args &open_args, long hinting_factor, - std::vector &fallback_list, - WarnFunc warn, bool warn_if_used); + using LanguageRange = std::tuple; + using LanguageType = std::optional>; + + FT2Font(long hinting_factor, std::vector &fallback_list, + bool warn_if_used); virtual ~FT2Font(); + void open(FT_Open_Args &open_args, FT_Long face_index); + void close(); void clear(); void set_size(double ptsize, double dpi); + void _set_transform( + std::array, 2> matrix, std::array delta); void set_charmap(int i); void select_charmap(unsigned long i); + std::vector layout(std::u32string_view text, FT_Int32 flags, + std::optional> features, + LanguageType languages, + std::set& glyph_seen_fonts); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, - std::vector &xys); - int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback); - int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta); + std::optional> features, + LanguageType languages, std::vector &xys); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_UInt &final_glyph_index, std::vector &parent_glyphs, std::unordered_map &parent_char_to_font, - std::unordered_map &parent_glyph_to_font, long charcode, FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, - std::set &glyph_seen_fonts, - bool override); - void load_glyph(FT_UInt glyph_index, FT_Int32 flags, FT2Font *&ft_object, bool fallback); + std::set &glyph_seen_fonts); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); - void get_width_height(long *width, long *height); - void get_bitmap_offset(long *x, long *y); + std::tuple get_width_height(); + std::tuple get_bitmap_offset(); long get_descent(); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap( py::array_t im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); + std::string get_glyph_name(unsigned int glyph_number); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); void get_path(std::vector &vertices, std::vector &codes); @@ -148,6 +158,10 @@ class FT2Font { return image; } + std::vector &get_glyphs() + { + return glyphs; + } FT_Glyph const &get_last_glyph() const { return glyphs.back(); @@ -169,15 +183,15 @@ class FT2Font return FT_HAS_KERNING(face); } + protected: + virtual void ft_glyph_warn(FT_ULong charcode, std::set family_names) = 0; private: - WarnFunc ft_glyph_warn; bool warn_if_used; py::array_t image; FT_Face face; FT_Vector pen; /* untransformed origin */ std::vector glyphs; std::vector fallbacks; - std::unordered_map glyph_to_font; std::unordered_map char_to_font; FT_BBox bbox; FT_Pos advance; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index ca2db6aa0e5b..bf345cd1d044 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -204,6 +204,25 @@ P11X_DECLARE_ENUM( {"TARGET_LCD_V", LoadFlags::TARGET_LCD_V}, ); +const char *RenderMode__doc__ = R"""( + Render modes. + + For more information, see `the FreeType documentation + `_. + + .. versionadded:: 3.10 +)"""; + +P11X_DECLARE_ENUM( + "RenderMode", "Enum", + {"NORMAL", FT_RENDER_MODE_NORMAL}, + {"LIGHT", FT_RENDER_MODE_LIGHT}, + {"MONO", FT_RENDER_MODE_MONO}, + {"LCD", FT_RENDER_MODE_LCD}, + {"LCD_V", FT_RENDER_MODE_LCD_V}, + {"SDF", FT_RENDER_MODE_SDF}, +); + const char *StyleFlags__doc__ = R"""( Flags returned by `FT2Font.style_flags`. @@ -265,6 +284,45 @@ PyFT2Image_draw_rect_filled(FT2Image *self, self->draw_rect_filled(x0, y0, x1, y1); } +/********************************************************************** + * Positioned Bitmap; owns the FT_Bitmap! + * */ + +struct PyPositionedBitmap { + FT_Int left, top; + bool owning; + FT_Bitmap bitmap; + + PyPositionedBitmap(FT_GlyphSlot slot) : + left{slot->bitmap_left}, top{slot->bitmap_top}, owning{true} + { + FT_Bitmap_Init(&bitmap); + FT_CHECK(FT_Bitmap_Convert, _ft2Library, &slot->bitmap, &bitmap, 1); + } + + PyPositionedBitmap(FT_BitmapGlyph bg) : + left{bg->left}, top{bg->top}, owning{true} + { + FT_Bitmap_Init(&bitmap); + FT_CHECK(FT_Bitmap_Convert, _ft2Library, &bg->bitmap, &bitmap, 1); + } + + PyPositionedBitmap(PyPositionedBitmap& other) = delete; // Non-copyable. + + PyPositionedBitmap(PyPositionedBitmap&& other) : + left{other.left}, top{other.top}, owning{true}, bitmap{other.bitmap} + { + other.owning = false; // Prevent double deletion. + } + + ~PyPositionedBitmap() + { + if (owning) { + FT_Bitmap_Done(_ft2Library, &bitmap); + } + } +}; + /********************************************************************** * Glyph * */ @@ -331,16 +389,35 @@ PyGlyph_get_bbox(PyGlyph *self) * FT2Font * */ -struct PyFT2Font +class PyFT2Font final : public FT2Font { - FT2Font *x; + public: + using FT2Font::FT2Font; + py::object py_file; FT_StreamRec stream; py::list fallbacks; ~PyFT2Font() { - delete this->x; + // Because destructors are called from subclass up to base class, we need to + // explicitly close the font here. Otherwise, the instance attributes here will + // be destroyed before the font itself, but those are used in the close callback. + close(); + } + + void ft_glyph_warn(FT_ULong charcode, std::set family_names) + { + std::set::iterator it = family_names.begin(); + std::stringstream ss; + ss<<*it; + while(++it != family_names.end()){ + ss<<", "<<*it; + } + + auto text_helpers = py::module_::import("matplotlib._text_helpers"); + auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); + warn_on_missing_glyph(charcode, ss.str()); } }; @@ -402,42 +479,24 @@ close_file_callback(FT_Stream stream) PyErr_Restore(type, value, traceback); } -static void -ft_glyph_warn(FT_ULong charcode, std::set family_names) -{ - std::set::iterator it = family_names.begin(); - std::stringstream ss; - ss<<*it; - while(++it != family_names.end()){ - ss<<", "<<*it; - } - - auto text_helpers = py::module_::import("matplotlib._text_helpers"); - auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); - warn_on_missing_glyph(charcode, ss.str()); -} - const char *PyFT2Font_init__doc__ = R"""( Parameters ---------- - filename : str or file-like + filename : str, bytes, os.PathLike, or io.BinaryIO The source of the font data in a format (ttf or ttc) that FreeType can read. hinting_factor : int, optional Must be positive. Used to scale the hinting in the x-direction. + face_index : int, optional + The index of the face in the font file to load. + _fallback_list : list of FT2Font, optional A list of FT2Font objects used to find missing glyphs. .. warning:: This API is both private and provisional: do not use it directly. - _kerning_factor : int, optional - Used to adjust the degree of kerning. - - .. warning:: - This API is private: do not use it directly. - _warn_if_used : bool, optional Used to trigger missing glyph warnings. @@ -446,16 +505,43 @@ const char *PyFT2Font_init__doc__ = R"""( )"""; static PyFT2Font * -PyFT2Font_init(py::object filename, long hinting_factor = 8, +PyFT2Font_init(py::object filename, long hinting_factor = 8, FT_Long face_index = 0, std::optional> fallback_list = std::nullopt, - int kerning_factor = 0, bool warn_if_used = false) + std::optional kerning_factor = std::nullopt, + bool warn_if_used = false) { if (hinting_factor <= 0) { throw py::value_error("hinting_factor must be greater than 0"); } + if (kerning_factor) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.11", "name"_a="_kerning_factor", "obj_type"_a="parameter"); + } else { + kerning_factor = 0; + } + + if (face_index < 0 || face_index > 0xffff) { + throw std::range_error("face_index must be between 0 and 65535, inclusive"); + } + + std::vector fallback_fonts; + if (fallback_list) { + // go through fallbacks to add them to our lists + std::copy(fallback_list->begin(), fallback_list->end(), + std::back_inserter(fallback_fonts)); + } + + auto self = new PyFT2Font(hinting_factor, fallback_fonts, warn_if_used); + self->set_kerning_factor(*kerning_factor); + + if (fallback_list) { + // go through fallbacks to add them to our lists + for (auto item : *fallback_list) { + self->fallbacks.append(item); + } + } - PyFT2Font *self = new PyFT2Font(); - self->x = nullptr; memset(&self->stream, 0, sizeof(FT_StreamRec)); self->stream.base = nullptr; self->stream.size = 0x7fffffff; // Unknown size. @@ -467,19 +553,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; - std::vector fallback_fonts; - if (fallback_list) { - // go through fallbacks to add them to our lists - for (auto item : *fallback_list) { - self->fallbacks.append(item); - // Also (locally) cache the underlying FT2Font objects. As long as - // the Python objects are kept alive, these pointer are good. - FT2Font *fback = item->x; - fallback_fonts.push_back(fback); - } - } - - if (py::isinstance(filename) || py::isinstance(filename)) { + auto PathLike = py::module_::import("os").attr("PathLike"); + if (py::isinstance(filename) || py::isinstance(filename) || + py::isinstance(filename, PathLike)) + { self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; } else { @@ -497,33 +574,24 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->stream.close = nullptr; } - self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn, - warn_if_used); - - self->x->set_kerning_factor(kerning_factor); + self->open(open_args, face_index); return self; } -static py::str +static py::object PyFT2Font_fname(PyFT2Font *self) { - if (self->stream.close) { // Called passed a filename to the constructor. + if (self->stream.close) { // User passed a filename to the constructor. return self->py_file.attr("name"); } else { - return py::cast(self->py_file); + return self->py_file; } } const char *PyFT2Font_clear__doc__ = "Clear all the glyphs, reset for a new call to `.set_text`."; -static void -PyFT2Font_clear(PyFT2Font *self) -{ - self->x->clear(); -} - const char *PyFT2Font_set_size__doc__ = R"""( Set the size of the text. @@ -535,11 +603,21 @@ const char *PyFT2Font_set_size__doc__ = R"""( The DPI used for rendering the text. )"""; -static void -PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) -{ - self->x->set_size(ptsize, dpi); -} +const char *PyFT2Font__set_transform__doc__ = R"""( + Set the transform of the text. + + This is a low-level function, where *matrix* and *delta* are directly in + 16.16 and 26.6 formats respectively. Refer to the FreeType docs of + FT_Set_Transform for further description. + + Note, every call to `.font_manager.get_font` will reset the transform to the default + to ensure consistency across cache accesses. + + Parameters + ---------- + matrix : (2, 2) array of int + delta : (2,) array of int +)"""; const char *PyFT2Font_set_charmap__doc__ = R"""( Make the i-th charmap current. @@ -559,12 +637,6 @@ const char *PyFT2Font_set_charmap__doc__ = R"""( .get_charmap )"""; -static void -PyFT2Font_set_charmap(PyFT2Font *self, int i) -{ - self->x->set_charmap(i); -} - const char *PyFT2Font_select_charmap__doc__ = R"""( Select a charmap by its FT_Encoding number. @@ -583,12 +655,6 @@ const char *PyFT2Font_select_charmap__doc__ = R"""( .get_charmap )"""; -static void -PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) -{ - self->x->select_charmap(i); -} - const char *PyFT2Font_get_kerning__doc__ = R"""( Get the kerning between two glyphs. @@ -618,7 +684,6 @@ static int PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, std::variant mode_or_int) { - bool fallback = true; FT_Kerning_Mode mode; if (auto value = std::get_if(&mode_or_int)) { @@ -636,55 +701,7 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, throw py::type_error("mode must be Kerning or int"); } - return self->x->get_kerning(left, right, mode, fallback); -} - -const char *PyFT2Font_get_fontmap__doc__ = R"""( - Get a mapping between characters and the font that includes them. - - .. warning:: - This API uses the fallback list and is both private and provisional: do not use - it directly. - - Parameters - ---------- - text : str - The characters for which to find fonts. - - Returns - ------- - dict[str, FT2Font] - A dictionary mapping unicode characters to `.FT2Font` objects. -)"""; - -static py::dict -PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) -{ - std::set codepoints; - - py::dict char_to_font; - for (auto code : text) { - if (!codepoints.insert(code).second) { - continue; - } - - py::object target_font; - int index; - if (self->x->get_char_fallback_index(code, index)) { - if (index >= 0) { - target_font = self->fallbacks[index]; - } else { - target_font = py::cast(self); - } - } else { - // TODO Handle recursion! - target_font = py::cast(self); - } - - auto key = py::cast(std::u32string(1, code)); - char_to_font[key] = target_font; - } - return char_to_font; + return self->get_kerning(left, right, mode); } const char *PyFT2Font_set_text__doc__ = R"""( @@ -703,6 +720,13 @@ const char *PyFT2Font_set_text__doc__ = R"""( .. versionchanged:: 3.10 This now takes an `.ft2font.LoadFlags` instead of an int. + features : tuple[str, ...] + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + + .. versionadded:: 3.11 Returns ------- @@ -712,7 +736,9 @@ const char *PyFT2Font_set_text__doc__ = R"""( static py::array_t PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, - std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT, + std::optional> features = std::nullopt, + std::variant languages_or_str = nullptr) { std::vector xys; LoadFlags flags; @@ -732,7 +758,21 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 throw py::type_error("flags must be LoadFlags or int"); } - self->x->set_text(text, angle, static_cast(flags), xys); + FT2Font::LanguageType languages; + if (auto value = std::get_if(&languages_or_str)) { + languages = std::move(*value); + } else if (auto value = std::get_if(&languages_or_str)) { + languages = std::vector{ + FT2Font::LanguageRange{*value, 0, text.size()} + }; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("languages must be str or list of tuple"); + } + + self->set_text(text, angle, static_cast(flags), features, languages, xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -744,12 +784,6 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 const char *PyFT2Font_get_num_glyphs__doc__ = "Return the number of loaded glyphs."; -static size_t -PyFT2Font_get_num_glyphs(PyFT2Font *self) -{ - return self->x->get_num_glyphs(); -} - const char *PyFT2Font_load_char__doc__ = R"""( Load character in current fontfile and set glyph. @@ -799,7 +833,7 @@ PyFT2Font_load_char(PyFT2Font *self, long charcode, throw py::type_error("flags must be LoadFlags or int"); } - self->x->load_char(charcode, static_cast(flags), ft_object, fallback); + self->load_char(charcode, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } @@ -834,8 +868,6 @@ static PyGlyph * PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { - bool fallback = true; - FT2Font *ft_object = nullptr; LoadFlags flags; if (auto value = std::get_if(&flags_or_int)) { @@ -853,9 +885,9 @@ PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, throw py::type_error("flags must be LoadFlags or int"); } - self->x->load_glyph(glyph_index, static_cast(flags), ft_object, fallback); + self->load_glyph(glyph_index, static_cast(flags)); - return PyGlyph_from_FT2Font(ft_object); + return PyGlyph_from_FT2Font(self); } const char *PyFT2Font_get_width_height__doc__ = R"""( @@ -875,16 +907,6 @@ const char *PyFT2Font_get_width_height__doc__ = R"""( .get_descent )"""; -static py::tuple -PyFT2Font_get_width_height(PyFT2Font *self) -{ - long width, height; - - self->x->get_width_height(&width, &height); - - return py::make_tuple(width, height); -} - const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( Get the (x, y) offset for the bitmap if ink hangs left or below (0, 0). @@ -902,16 +924,6 @@ const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( .get_descent )"""; -static py::tuple -PyFT2Font_get_bitmap_offset(PyFT2Font *self) -{ - long x, y; - - self->x->get_bitmap_offset(&x, &y); - - return py::make_tuple(x, y); -} - const char *PyFT2Font_get_descent__doc__ = R"""( Get the descent of the current string set by `.set_text`. @@ -929,12 +941,6 @@ const char *PyFT2Font_get_descent__doc__ = R"""( .get_width_height )"""; -static long -PyFT2Font_get_descent(PyFT2Font *self) -{ - return self->x->get_descent(); -} - const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( Draw the glyphs that were loaded by `.set_text` to the bitmap. @@ -950,12 +956,6 @@ const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( .draw_glyph_to_bitmap )"""; -static void -PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) -{ - self->x->draw_glyphs_to_bitmap(antialiased); -} - const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( Draw a single glyph to the bitmap at pixel locations x, y. @@ -971,7 +971,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( image : 2d array of uint8 The image buffer on which to draw the glyph. x, y : int - The pixel location at which to draw the glyph. + The position of the glyph's top left corner. glyph : Glyph The glyph to draw. antialiased : bool, default: True @@ -990,7 +990,7 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image, auto xd = _double_to_("x", vxd); auto yd = _double_to_("y", vyd); - self->x->draw_glyph_to_bitmap( + self->draw_glyph_to_bitmap( py::array_t{image}, xd, yd, glyph->glyphInd, antialiased); } @@ -1018,17 +1018,6 @@ const char *PyFT2Font_get_glyph_name__doc__ = R"""( .get_name_index )"""; -static py::str -PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) -{ - std::string buffer; - bool fallback = true; - - buffer.resize(128); - self->x->get_glyph_name(glyph_number, buffer, fallback); - return buffer; -} - const char *PyFT2Font_get_charmap__doc__ = R"""( Return a mapping of character codes to glyph indices in the font. @@ -1047,10 +1036,10 @@ PyFT2Font_get_charmap(PyFT2Font *self) { py::dict charmap; FT_UInt index; - FT_ULong code = FT_Get_First_Char(self->x->get_face(), &index); + FT_ULong code = FT_Get_First_Char(self->get_face(), &index); while (index != 0) { charmap[py::cast(code)] = py::cast(index); - code = FT_Get_Next_Char(self->x->get_face(), code, &index); + code = FT_Get_Next_Char(self->get_face(), code, &index); } return charmap; } @@ -1062,6 +1051,8 @@ const char *PyFT2Font_get_char_index__doc__ = R"""( ---------- codepoint : int A character code point in the current charmap (which defaults to Unicode.) + _fallback : bool + Whether to enable fallback fonts while searching for a character. Returns ------- @@ -1076,14 +1067,6 @@ const char *PyFT2Font_get_char_index__doc__ = R"""( .get_name_index )"""; -static FT_UInt -PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) -{ - bool fallback = true; - - return self->x->get_char_index(ccode, fallback); -} - const char *PyFT2Font_get_sfnt__doc__ = R"""( Load the entire SFNT names table. @@ -1100,17 +1083,17 @@ const char *PyFT2Font_get_sfnt__doc__ = R"""( static py::dict PyFT2Font_get_sfnt(PyFT2Font *self) { - if (!(self->x->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { + if (!(self->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { throw py::value_error("No SFNT name table"); } - size_t count = FT_Get_Sfnt_Name_Count(self->x->get_face()); + size_t count = FT_Get_Sfnt_Name_Count(self->get_face()); py::dict names; for (FT_UInt j = 0; j < count; ++j) { FT_SfntName sfnt; - FT_Error error = FT_Get_Sfnt_Name(self->x->get_face(), j, &sfnt); + FT_Error error = FT_Get_Sfnt_Name(self->get_face(), j, &sfnt); if (error) { throw py::value_error("Could not get SFNT name"); @@ -1145,12 +1128,6 @@ const char *PyFT2Font_get_name_index__doc__ = R"""( .get_glyph_name )"""; -static long -PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) -{ - return self->x->get_name_index(glyphname); -} - const char *PyFT2Font_get_ps_font_info__doc__ = R"""( Return the information in the PS Font Info structure. @@ -1175,7 +1152,7 @@ PyFT2Font_get_ps_font_info(PyFT2Font *self) { PS_FontInfoRec fontinfo; - FT_Error error = FT_Get_PS_Font_Info(self->x->get_face(), &fontinfo); + FT_Error error = FT_Get_PS_Font_Info(self->get_face(), &fontinfo); if (error) { throw py::value_error("Could not get PS font info"); } @@ -1227,7 +1204,7 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) return std::nullopt; } - void *table = FT_Get_Sfnt_Table(self->x->get_face(), tag); + void *table = FT_Get_Sfnt_Table(self->get_face(), tag); if (!table) { return std::nullopt; } @@ -1286,8 +1263,9 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) } case FT_SFNT_OS2: { auto t = static_cast(table); - return py::dict( - "version"_a=t->version, + auto version = t->version; + auto result = py::dict( + "version"_a=version, "xAvgCharWidth"_a=t->xAvgCharWidth, "usWeightClass"_a=t->usWeightClass, "usWidthClass"_a=t->usWidthClass, @@ -1304,12 +1282,33 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) "yStrikeoutPosition"_a=t->yStrikeoutPosition, "sFamilyClass"_a=t->sFamilyClass, "panose"_a=py::bytes(reinterpret_cast(t->panose), 10), - "ulCharRange"_a=py::make_tuple(t->ulUnicodeRange1, t->ulUnicodeRange2, - t->ulUnicodeRange3, t->ulUnicodeRange4), + "ulUnicodeRange"_a=py::make_tuple(t->ulUnicodeRange1, t->ulUnicodeRange2, + t->ulUnicodeRange3, t->ulUnicodeRange4), "achVendID"_a=py::bytes(reinterpret_cast(t->achVendID), 4), "fsSelection"_a=t->fsSelection, - "fsFirstCharIndex"_a=t->usFirstCharIndex, - "fsLastCharIndex"_a=t->usLastCharIndex); + "usFirstCharIndex"_a=t->usFirstCharIndex, + "usLastCharIndex"_a=t->usLastCharIndex, + "sTypoAscender"_a=t->sTypoAscender, + "sTypoDescender"_a=t->sTypoDescender, + "sTypoLineGap"_a=t->sTypoLineGap, + "usWinAscent"_a=t->usWinAscent, + "usWinDescent"_a=t->usWinDescent); + if (version >= 1) { + result["ulCodePageRange"] = py::make_tuple(t->ulCodePageRange1, + t->ulCodePageRange2); + } + if (version >= 2) { + result["sxHeight"] = t->sxHeight; + result["sCapHeight"] = t->sCapHeight; + result["usDefaultChar"] = t->usDefaultChar; + result["usBreakChar"] = t->usBreakChar; + result["usMaxContext"] = t->usMaxContext; + } + if (version >= 5) { + result["usLowerOpticalPointSize"] = t->usLowerOpticalPointSize; + result["usUpperOpticalPointSize"] = t->usUpperOpticalPointSize; + } + return result; } case FT_SFNT_HHEA: { auto t = static_cast(table); @@ -1339,7 +1338,7 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) "vertTypoLineGap"_a=t->Line_Gap, "advanceHeightMax"_a=t->advance_Height_Max, "minTopSideBearing"_a=t->min_Top_Side_Bearing, - "minBottomSizeBearing"_a=t->min_Bottom_Side_Bearing, + "minBottomSideBearing"_a=t->min_Bottom_Side_Bearing, "yMaxExtent"_a=t->yMax_Extent, "caretSlopeRise"_a=t->caret_Slope_Rise, "caretSlopeRun"_a=t->caret_Slope_Run, @@ -1410,7 +1409,7 @@ PyFT2Font_get_path(PyFT2Font *self) std::vector vertices; std::vector codes; - self->x->get_path(vertices, codes); + self->get_path(vertices, codes); py::ssize_t length = codes.size(); py::ssize_t vertices_dims[2] = { length, 2 }; @@ -1439,12 +1438,6 @@ const char *PyFT2Font_get_image__doc__ = R"""( .get_path )"""; -static py::array -PyFT2Font_get_image(PyFT2Font *self) -{ - return self->x->get_image(); -} - const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""( Return a list mapping CharString indices of a Type 1 font to FreeType glyph indices. @@ -1456,7 +1449,7 @@ const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""( static std::array PyFT2Font__get_type1_encoding_vector(PyFT2Font *self) { - auto face = self->x->get_face(); + auto face = self->get_face(); auto indices = std::array{}; for (auto i = 0u; i < indices.size(); ++i) { auto len = FT_Get_PS_Font_Value(face, PS_DICT_ENCODING_ENTRY, i, nullptr, 0); @@ -1471,6 +1464,119 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self) return indices; } +/********************************************************************** + * Layout items + * */ + +struct LayoutItem { + PyFT2Font *ft_object; + std::u32string character; + int glyph_index; + double x; + double y; + double prev_kern; + + LayoutItem(PyFT2Font *f, std::u32string c, int i, double x, double y, double k) : + ft_object(f), character(c), glyph_index(i), x(x), y(y), prev_kern(k) {} +}; + +const char *PyFT2Font_layout__doc__ = R"""( + Layout a string and yield information about each used glyph. + + .. warning:: + This API uses the fallback list and is both private and provisional: do not use + it directly. + + .. versionadded:: 3.11 + + Parameters + ---------- + text : str + The characters for which to find fonts. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + Returns + ------- + list[LayoutItem] +)"""; + +static auto +PyFT2Font_layout(PyFT2Font *self, std::u32string text, LoadFlags flags, + std::optional> features = std::nullopt, + std::variant languages_or_str = nullptr) +{ + const auto hinting_factor = self->get_hinting_factor(); + const auto load_flags = static_cast(flags); + + FT2Font::LanguageType languages; + if (auto value = std::get_if(&languages_or_str)) { + languages = std::move(*value); + } else if (auto value = std::get_if(&languages_or_str)) { + languages = std::vector{ + FT2Font::LanguageRange{*value, 0, text.size()} + }; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("languages must be str or list of tuple"); + } + + std::set glyph_seen_fonts; + auto glyphs = self->layout(text, load_flags, features, languages, glyph_seen_fonts); + + std::set clusters; + for (auto &glyph : glyphs) { + clusters.emplace(glyph.cluster); + } + + std::vector items; + + double x = 0.0; + double y = 0.0; + std::optional prev_advance = std::nullopt; + double prev_x = 0.0; + for (auto &glyph : glyphs) { + auto ft_object = static_cast(glyph.ftface->generic.data); + + ft_object->load_glyph(glyph.index, load_flags); + + double prev_kern = 0.0; + if (prev_advance) { + double actual_advance = (x + glyph.x_offset) - prev_x; + prev_kern = actual_advance - *prev_advance; + } + + auto next = clusters.upper_bound(glyph.cluster); + auto end = (next != clusters.end()) ? *next : text.size(); + auto substr = text.substr(glyph.cluster, end - glyph.cluster); + + items.emplace_back(ft_object, substr, glyph.index, + (x + glyph.x_offset) / 64.0, (y + glyph.y_offset) / 64.0, + prev_kern / 64.0); + prev_x = x + glyph.x_offset; + x += glyph.x_advance; + y += glyph.y_advance; + // Note, linearHoriAdvance is a 16.16 instead of 26.6 fixed-point value. + prev_advance = ft_object->get_face()->glyph->linearHoriAdvance / 1024.0 / hinting_factor; + } + + return items; +} + +/********************************************************************** + * Deprecations + * */ + static py::object ft2font__getattr__(std::string name) { auto api = py::module_::import("matplotlib._api"); @@ -1555,6 +1661,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) p11x::bind_enums(m); p11x::enums["Kerning"].attr("__doc__") = Kerning__doc__; p11x::enums["LoadFlags"].attr("__doc__") = LoadFlags__doc__; + p11x::enums["RenderMode"].attr("__doc__") = RenderMode__doc__; p11x::enums["FaceFlags"].attr("__doc__") = FaceFlags__doc__; p11x::enums["StyleFlags"].attr("__doc__") = StyleFlags__doc__; @@ -1581,6 +1688,17 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) return py::buffer_info(self.get_buffer(), shape, strides); }); + py::class_(m, "_PositionedBitmap", py::is_final()) + .def_readonly("left", &PyPositionedBitmap::left) + .def_readonly("top", &PyPositionedBitmap::top) + .def_property_readonly( + "buffer", [](PyPositionedBitmap &self) -> py::array { + return {{self.bitmap.rows, self.bitmap.width}, + {self.bitmap.pitch, 1}, + self.bitmap.buffer}; + }) + ; + py::class_(m, "Glyph", py::is_final(), PyGlyph__doc__) .def(py::init<>([]() -> PyGlyph { // Glyph is not useful from Python, so mark it as not constructible. @@ -1605,40 +1723,71 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def_property_readonly("bbox", &PyGlyph_get_bbox, "The control box of the glyph."); + py::class_(m, "LayoutItem", py::is_final()) + .def(py::init<>([]() -> LayoutItem { + // LayoutItem is not useful from Python, so mark it as not constructible. + throw std::runtime_error("LayoutItem is not constructible"); + })) + .def_readonly("ft_object", &LayoutItem::ft_object, + "The FT_Face of the item.") + .def_readonly("char", &LayoutItem::character, + "The character code for the item.") + .def_readonly("glyph_index", &LayoutItem::glyph_index, + "The glyph index for the item.") + .def_readonly("x", &LayoutItem::x, + "The x position of the item.") + .def_readonly("y", &LayoutItem::y, + "The y position of the item.") + .def_readonly("prev_kern", &LayoutItem::prev_kern, + "The kerning between this item and the previous one.") + .def("__str__", + [](const LayoutItem& item) { + return + "LayoutItem(ft_object={}, char={!r}, glyph_index={}, "_s + "x={}, y={}, prev_kern={})"_s.format( + PyFT2Font_fname(item.ft_object), item.character, + item.glyph_index, item.x, item.y, item.prev_kern); + }); + auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), - "filename"_a, "hinting_factor"_a=8, py::kw_only(), - "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, + "filename"_a, "hinting_factor"_a=8, py::kw_only(), "face_index"_a=0, + "_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(), "_warn_if_used"_a=false, PyFT2Font_init__doc__) - .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) - .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a, + .def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__) + .def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a, PyFT2Font_set_size__doc__) - .def("set_charmap", &PyFT2Font_set_charmap, "i"_a, + .def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a, + PyFT2Font__set_transform__doc__) + .def("set_charmap", &PyFT2Font::set_charmap, "i"_a, PyFT2Font_set_charmap__doc__) - .def("select_charmap", &PyFT2Font_select_charmap, "i"_a, + .def("select_charmap", &PyFT2Font::select_charmap, "i"_a, PyFT2Font_select_charmap__doc__) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) + .def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(), + "features"_a=nullptr, "language"_a=nullptr, + PyFT2Font_layout__doc__) .def("set_text", &PyFT2Font_set_text, - "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, + "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), + "features"_a=nullptr, "language"_a=nullptr, PyFT2Font_set_text__doc__) - .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, - PyFT2Font_get_fontmap__doc__) - .def("get_num_glyphs", &PyFT2Font_get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) + .def("get_num_glyphs", &PyFT2Font::get_num_glyphs, + PyFT2Font_get_num_glyphs__doc__) .def("load_char", &PyFT2Font_load_char, "charcode"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_char__doc__) .def("load_glyph", &PyFT2Font_load_glyph, "glyph_index"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_glyph__doc__) - .def("get_width_height", &PyFT2Font_get_width_height, + .def("get_width_height", &PyFT2Font::get_width_height, PyFT2Font_get_width_height__doc__) - .def("get_bitmap_offset", &PyFT2Font_get_bitmap_offset, + .def("get_bitmap_offset", &PyFT2Font::get_bitmap_offset, PyFT2Font_get_bitmap_offset__doc__) - .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) - .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, + .def("get_descent", &PyFT2Font::get_descent, PyFT2Font_get_descent__doc__) + .def("draw_glyphs_to_bitmap", &PyFT2Font::draw_glyphs_to_bitmap, py::kw_only(), "antialiased"_a=true, PyFT2Font_draw_glyphs_to_bitmap__doc__); // The generated docstring uses an unqualified "Buffer" as type hint, @@ -1654,26 +1803,27 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_draw_glyph_to_bitmap__doc__); } cls - .def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a, + .def("get_glyph_name", &PyFT2Font::get_glyph_name, "index"_a, PyFT2Font_get_glyph_name__doc__) .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__) - .def("get_char_index", &PyFT2Font_get_char_index, "codepoint"_a, + .def("get_char_index", &PyFT2Font::get_char_index, + "codepoint"_a, py::kw_only(), "_fallback"_a=true, PyFT2Font_get_char_index__doc__) .def("get_sfnt", &PyFT2Font_get_sfnt, PyFT2Font_get_sfnt__doc__) - .def("get_name_index", &PyFT2Font_get_name_index, "name"_a, + .def("get_name_index", &PyFT2Font::get_name_index, "name"_a, PyFT2Font_get_name_index__doc__) .def("get_ps_font_info", &PyFT2Font_get_ps_font_info, PyFT2Font_get_ps_font_info__doc__) .def("get_sfnt_table", &PyFT2Font_get_sfnt_table, "name"_a, PyFT2Font_get_sfnt_table__doc__) .def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__) - .def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__) + .def("get_image", &PyFT2Font::get_image, PyFT2Font_get_image__doc__) .def("_get_type1_encoding_vector", &PyFT2Font__get_type1_encoding_vector, PyFT2Font__get_type1_encoding_vector__doc__) .def_property_readonly( "postscript_name", [](PyFT2Font *self) { - if (const char *name = FT_Get_Postscript_Name(self->x->get_face())) { + if (const char *name = FT_Get_Postscript_Name(self->get_face())) { return name; } else { return "UNAVAILABLE"; @@ -1681,11 +1831,15 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "PostScript name of the font.") .def_property_readonly( "num_faces", [](PyFT2Font *self) { - return self->x->get_face()->num_faces; + return self->get_face()->num_faces & 0xffff; }, "Number of faces in file.") + .def_property_readonly( + "face_index", [](PyFT2Font *self) { + return self->get_face()->face_index; + }, "The index of the font in the file.") .def_property_readonly( "family_name", [](PyFT2Font *self) { - if (const char *name = self->x->get_face()->family_name) { + if (const char *name = self->get_face()->family_name) { return name; } else { return "UNAVAILABLE"; @@ -1693,7 +1847,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "Face family name.") .def_property_readonly( "style_name", [](PyFT2Font *self) { - if (const char *name = self->x->get_face()->style_name) { + if (const char *name = self->get_face()->style_name) { return name; } else { return "UNAVAILABLE"; @@ -1701,80 +1855,96 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "Style name.") .def_property_readonly( "face_flags", [](PyFT2Font *self) { - return static_cast(self->x->get_face()->face_flags); + return static_cast(self->get_face()->face_flags); }, "Face flags; see `.FaceFlags`.") .def_property_readonly( "style_flags", [](PyFT2Font *self) { - return static_cast(self->x->get_face()->style_flags & 0xffff); + return static_cast(self->get_face()->style_flags & 0xffff); }, "Style flags; see `.StyleFlags`.") .def_property_readonly( "num_named_instances", [](PyFT2Font *self) { - return (self->x->get_face()->style_flags & 0x7fff0000) >> 16; + return (self->get_face()->style_flags & 0x7fff0000) >> 16; }, "Number of named instances in the face.") .def_property_readonly( "num_glyphs", [](PyFT2Font *self) { - return self->x->get_face()->num_glyphs; + return self->get_face()->num_glyphs; }, "Number of glyphs in the face.") .def_property_readonly( "num_fixed_sizes", [](PyFT2Font *self) { - return self->x->get_face()->num_fixed_sizes; + return self->get_face()->num_fixed_sizes; }, "Number of bitmap in the face.") .def_property_readonly( "num_charmaps", [](PyFT2Font *self) { - return self->x->get_face()->num_charmaps; + return self->get_face()->num_charmaps; }, "Number of charmaps in the face.") .def_property_readonly( "scalable", [](PyFT2Font *self) { - return bool(FT_IS_SCALABLE(self->x->get_face())); + return bool(FT_IS_SCALABLE(self->get_face())); }, "Whether face is scalable; attributes after this one " "are only defined for scalable faces.") .def_property_readonly( "units_per_EM", [](PyFT2Font *self) { - return self->x->get_face()->units_per_EM; + return self->get_face()->units_per_EM; }, "Number of font units covered by the EM.") .def_property_readonly( "bbox", [](PyFT2Font *self) { - FT_BBox bbox = self->x->get_face()->bbox; + FT_BBox bbox = self->get_face()->bbox; return py::make_tuple(bbox.xMin, bbox.yMin, bbox.xMax, bbox.yMax); }, "Face global bounding box (xmin, ymin, xmax, ymax).") .def_property_readonly( "ascender", [](PyFT2Font *self) { - return self->x->get_face()->ascender; + return self->get_face()->ascender; }, "Ascender in 26.6 units.") .def_property_readonly( "descender", [](PyFT2Font *self) { - return self->x->get_face()->descender; + return self->get_face()->descender; }, "Descender in 26.6 units.") .def_property_readonly( "height", [](PyFT2Font *self) { - return self->x->get_face()->height; + return self->get_face()->height; }, "Height in 26.6 units; used to compute a default line spacing " "(baseline-to-baseline distance).") .def_property_readonly( "max_advance_width", [](PyFT2Font *self) { - return self->x->get_face()->max_advance_width; + return self->get_face()->max_advance_width; }, "Maximum horizontal cursor advance for all glyphs.") .def_property_readonly( "max_advance_height", [](PyFT2Font *self) { - return self->x->get_face()->max_advance_height; + return self->get_face()->max_advance_height; }, "Maximum vertical cursor advance for all glyphs.") .def_property_readonly( "underline_position", [](PyFT2Font *self) { - return self->x->get_face()->underline_position; + return self->get_face()->underline_position; }, "Vertical position of the underline bar.") .def_property_readonly( "underline_thickness", [](PyFT2Font *self) { - return self->x->get_face()->underline_thickness; + return self->get_face()->underline_thickness; }, "Thickness of the underline bar.") .def_property_readonly( "fname", &PyFT2Font_fname, "The original filename for this object.") + .def_property_readonly( + "_hinting_factor", &PyFT2Font::get_hinting_factor, + "The hinting factor.") .def_buffer([](PyFT2Font &self) -> py::buffer_info { - return self.x->get_image().request(); - }); + return self.get_image().request(); + }) + + .def("_render_glyph", + [](PyFT2Font *self, FT_UInt idx, LoadFlags flags, FT_Render_Mode render_mode) { + auto face = self->get_face(); + FT_CHECK(FT_Load_Glyph, face, idx, static_cast(flags)); + FT_CHECK(FT_Render_Glyph, face->glyph, render_mode); + return PyPositionedBitmap{face->glyph}; + }) + ; m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + m.attr("__libraqm_version__") = raqm_version_string(); + auto py_int = py::module_::import("builtins").attr("int"); + m.attr("CharacterCodeType") = py_int; + m.attr("GlyphIndexType") = py_int; m.def("__getattr__", ft2font__getattr__); } diff --git a/src/meson.build b/src/meson.build index d479a8b84aa2..8b52bf739c03 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,7 +53,7 @@ extension_data = { 'ft2font_wrapper.cpp', ), 'dependencies': [ - freetype_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, libraqm_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( diff --git a/subprojects/freetype-2.6.1.wrap b/subprojects/freetype-2.6.1.wrap deleted file mode 100644 index 270556f0d5d3..000000000000 --- a/subprojects/freetype-2.6.1.wrap +++ /dev/null @@ -1,10 +0,0 @@ -[wrap-file] -source_url = https://download.savannah.nongnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz -source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.6.1/freetype-2.6.1.tar.gz -source_filename = freetype-2.6.1.tar.gz -source_hash = 0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 - -patch_directory = freetype-2.6.1-meson - -[provide] -freetype-2.6.1 = freetype_dep diff --git a/subprojects/freetype2.wrap b/subprojects/freetype2.wrap new file mode 100644 index 000000000000..28b452035cf0 --- /dev/null +++ b/subprojects/freetype2.wrap @@ -0,0 +1,17 @@ +# This is the version of FreeType to use when building a local version. It must match +# the `LOCAL_FREETYPE_VERSION` value in `lib/matplotlib/__init__.py`. Bump the cache key +# in `.circleci/config.yml` when changing requirements. +[wrap-file] +directory = freetype-2.14.3 +source_url = https://download.savannah.nongnu.org/releases/freetype/freetype-2.14.3.tar.xz +source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.xz +source_filename = freetype-2.14.3.tar.xz +source_hash = 36bc4f1cc413335368ee656c42afca65c5a3987e8768cc28cf11ba775e785a5f + +# First patch allows using our bundled HarfBuzz. +# Second patch fixes symbol problems on wasm. +diff_files = freetype-2.14.1-static-harfbuzz.patch, freetype-2.14.1-wasm-visibility.patch + +[provide] +freetype2 = freetype_dep +freetype = freetype_dep diff --git a/subprojects/harfbuzz.wrap b/subprojects/harfbuzz.wrap new file mode 100644 index 000000000000..da0f7590a589 --- /dev/null +++ b/subprojects/harfbuzz.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = harfbuzz-12.3.0 +source_url = https://github.com/harfbuzz/harfbuzz/releases/download/12.3.0/harfbuzz-12.3.0.tar.xz +source_filename = harfbuzz-12.3.0.tar.xz +source_hash = 8660ebd3c27d9407fc8433b5d172bafba5f0317cb0bb4339f28e5370c93d42b7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/harfbuzz_12.3.0-1/harfbuzz-12.3.0.tar.xz +wrapdb_version = 12.3.0-1 + +[provide] +dependency_names = harfbuzz, harfbuzz-cairo, harfbuzz-gobject, harfbuzz-icu, harfbuzz-subset diff --git a/subprojects/libraqm-0.10.4.wrap b/subprojects/libraqm-0.10.4.wrap new file mode 100644 index 000000000000..5fad16334895 --- /dev/null +++ b/subprojects/libraqm-0.10.4.wrap @@ -0,0 +1,7 @@ +[wrap-file] +source_url = https://github.com/HOST-Oman/libraqm/archive/v0.10.4/libraqm-0.10.4.tar.gz +source_filename = libraqm-0.10.4.tar.gz +source_hash = 6b583fb0eb159a3727a1e8c653bb0294173a14af8eb60195a775879de72320a3 + +# First patch allows using our bundled FreeType. +diff_files = libraqm-0.10.2-bundle-freetype.patch diff --git a/subprojects/packagefiles/freetype-2.14.1-static-harfbuzz.patch b/subprojects/packagefiles/freetype-2.14.1-static-harfbuzz.patch new file mode 100644 index 000000000000..c09416a6e155 --- /dev/null +++ b/subprojects/packagefiles/freetype-2.14.1-static-harfbuzz.patch @@ -0,0 +1,29 @@ +diff -uPNr freetype-2.14.1.orig/meson.build freetype-2.14.1/meson.build +--- freetype-2.14.1.orig/meson.build 2025-09-11 07:12:24.000000000 -0400 ++++ freetype-2.14.1/meson.build 2026-01-04 15:49:14.198061441 -0500 +@@ -364,6 +364,13 @@ + endif + endif + ++if harfbuzz_opt == 'static' ++ harfbuzz_dep = declare_dependency() ++ harfbuzz_opt = 'YES' ++ ftoption_command += ['--enable=FT_CONFIG_OPTION_USE_HARFBUZZ'] ++ ft2_deps += [harfbuzz_dep] ++endif ++ + if not harfbuzz_dep.found() and \ + (harfbuzz_opt == 'dynamic' or harfbuzz_opt == 'auto') + # On Windows we don't need libdl, but on other platforms we need it. +diff -uPNr freetype-2.14.1.orig/meson_options.txt freetype-2.14.1/meson_options.txt +--- freetype-2.14.1.orig/meson_options.txt 2025-09-07 22:48:18.000000000 -0400 ++++ freetype-2.14.1/meson_options.txt 2026-01-04 15:49:30.087034418 -0500 +@@ -24,7 +24,7 @@ + + option('harfbuzz', + type: 'combo', +- choices: ['auto', 'enabled', 'dynamic', 'disabled'], ++ choices: ['auto', 'enabled', 'dynamic', 'static', 'disabled'], + value: 'auto', + description: 'Use Harfbuzz library to improve auto-hinting;' + + ' if available, many glyphs not directly addressable' diff --git a/subprojects/packagefiles/freetype-2.14.1-wasm-visibility.patch b/subprojects/packagefiles/freetype-2.14.1-wasm-visibility.patch new file mode 100644 index 000000000000..9c96e4191543 --- /dev/null +++ b/subprojects/packagefiles/freetype-2.14.1-wasm-visibility.patch @@ -0,0 +1,26 @@ +diff -uPNr freetype-2.14.3.orig/meson.build freetype-2.14.3/meson.build +--- freetype-2.14.3.orig/meson.build 2026-03-27 05:26:03.270830734 -0400 ++++ freetype-2.14.3/meson.build 2026-03-27 16:19:46.222942478 -0400 +@@ -453,16 +453,21 @@ + ft2_defines += ['-DFT_CONFIG_CONFIG_H='] + endif + ++if cc.get_id() == 'emscripten' ++ kwargs = {} ++else ++ kwargs = {'gnu_symbol_visibility': 'hidden'} ++endif + + ft2_lib = library('freetype', + sources: ft2_sources + [ftmodule_h], + c_args: ft2_defines, +- gnu_symbol_visibility: 'hidden', + include_directories: ft2_includes, + dependencies: ft2_deps, + install: true, + version: ft2_so_version, + link_args: common_ldflags, ++ kwargs: kwargs, + ) + + diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in b/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in deleted file mode 100644 index 400f3a2a5bf2..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in +++ /dev/null @@ -1,498 +0,0 @@ -/***************************************************************************/ -/* */ -/* ftconfig.in */ -/* */ -/* UNIX-specific configuration file (specification only). */ -/* */ -/* Copyright 1996-2015 by */ -/* David Turner, Robert Wilhelm, and Werner Lemberg. */ -/* */ -/* This file is part of the FreeType project, and may only be used, */ -/* modified, and distributed under the terms of the FreeType project */ -/* license, LICENSE.TXT. By continuing to use, modify, or distribute */ -/* this file you indicate that you have read the license and */ -/* understand and accept it fully. */ -/* */ -/***************************************************************************/ - - - /*************************************************************************/ - /* */ - /* This header file contains a number of macro definitions that are used */ - /* by the rest of the engine. Most of the macros here are automatically */ - /* determined at compile time, and you should not need to change it to */ - /* port FreeType, except to compile the library with a non-ANSI */ - /* compiler. */ - /* */ - /* Note however that if some specific modifications are needed, we */ - /* advise you to place a modified copy in your build directory. */ - /* */ - /* The build directory is usually `builds/', and contains */ - /* system-specific files that are always included first when building */ - /* the library. */ - /* */ - /*************************************************************************/ - -/* MESON: based on unix/ftconfig.in with but meson-friendly configuration defines */ - -#ifndef FTCONFIG_H_ -#define FTCONFIG_H_ - -#include -#include FT_CONFIG_OPTIONS_H -#include FT_CONFIG_STANDARD_LIBRARY_H - - -FT_BEGIN_HEADER - - - /*************************************************************************/ - /* */ - /* PLATFORM-SPECIFIC CONFIGURATION MACROS */ - /* */ - /* These macros can be toggled to suit a specific system. The current */ - /* ones are defaults used to compile FreeType in an ANSI C environment */ - /* (16bit compilers are also supported). Copy this file to your own */ - /* `builds/' directory, and edit it to port the engine. */ - /* */ - /*************************************************************************/ - - -#define HAVE_UNISTD_H @HAVE_UNISTD_H@ -#define HAVE_FCNTL_H @HAVE_FCNTL_H@ -#define HAVE_STDINT_H @HAVE_STDINT_H@ - - - /* There are systems (like the Texas Instruments 'C54x) where a `char' */ - /* has 16 bits. ANSI C says that sizeof(char) is always 1. Since an */ - /* `int' has 16 bits also for this system, sizeof(int) gives 1 which */ - /* is probably unexpected. */ - /* */ - /* `CHAR_BIT' (defined in limits.h) gives the number of bits in a */ - /* `char' type. */ - -#ifndef FT_CHAR_BIT -#define FT_CHAR_BIT CHAR_BIT -#endif - - -#undef FT_USE_AUTOCONF_SIZEOF_TYPES -#ifdef FT_USE_AUTOCONF_SIZEOF_TYPES - -#undef SIZEOF_INT -#undef SIZEOF_LONG -#define FT_SIZEOF_INT SIZEOF_INT -#define FT_SIZEOF_LONG SIZEOF_LONG - -#else /* !FT_USE_AUTOCONF_SIZEOF_TYPES */ - - /* Following cpp computation of the bit length of int and long */ - /* is copied from default include/freetype/config/ftconfig.h. */ - /* If any improvement is required for this file, it should be */ - /* applied to the original header file for the builders that */ - /* do not use configure script. */ - - /* The size of an `int' type. */ -#if FT_UINT_MAX == 0xFFFFUL -#define FT_SIZEOF_INT (16 / FT_CHAR_BIT) -#elif FT_UINT_MAX == 0xFFFFFFFFUL -#define FT_SIZEOF_INT (32 / FT_CHAR_BIT) -#elif FT_UINT_MAX > 0xFFFFFFFFUL && FT_UINT_MAX == 0xFFFFFFFFFFFFFFFFUL -#define FT_SIZEOF_INT (64 / FT_CHAR_BIT) -#else -#error "Unsupported size of `int' type!" -#endif - - /* The size of a `long' type. A five-byte `long' (as used e.g. on the */ - /* DM642) is recognized but avoided. */ -#if FT_ULONG_MAX == 0xFFFFFFFFUL -#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT) -#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFUL -#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT) -#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFFFFFFFUL -#define FT_SIZEOF_LONG (64 / FT_CHAR_BIT) -#else -#error "Unsupported size of `long' type!" -#endif - -#endif /* !FT_USE_AUTOCONF_SIZEOF_TYPES */ - - - /* FT_UNUSED is a macro used to indicate that a given parameter is not */ - /* used -- this is only used to get rid of unpleasant compiler warnings */ -#ifndef FT_UNUSED -#define FT_UNUSED( arg ) ( (arg) = (arg) ) -#endif - - - /*************************************************************************/ - /* */ - /* AUTOMATIC CONFIGURATION MACROS */ - /* */ - /* These macros are computed from the ones defined above. Don't touch */ - /* their definition, unless you know precisely what you are doing. No */ - /* porter should need to mess with them. */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Mac support */ - /* */ - /* This is the only necessary change, so it is defined here instead */ - /* providing a new configuration file. */ - /* */ -#if defined( __APPLE__ ) || ( defined( __MWERKS__ ) && defined( macintosh ) ) - /* no Carbon frameworks for 64bit 10.4.x */ - /* AvailabilityMacros.h is available since Mac OS X 10.2, */ - /* so guess the system version by maximum errno before inclusion */ -#include -#ifdef ECANCELED /* defined since 10.2 */ -#include "AvailabilityMacros.h" -#endif -#if defined( __LP64__ ) && \ - ( MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 ) -/undef FT_MACINTOSH -#endif - -#elif defined( __SC__ ) || defined( __MRC__ ) - /* Classic MacOS compilers */ -#include "ConditionalMacros.h" -#if TARGET_OS_MAC -#define FT_MACINTOSH 1 -#endif - -#endif - - - /* Fix compiler warning with sgi compiler */ -#if defined( __sgi ) && !defined( __GNUC__ ) -#if defined( _COMPILER_VERSION ) && ( _COMPILER_VERSION >= 730 ) -#pragma set woff 3505 -#endif -#endif - - - /*************************************************************************/ - /* */ - /*
*/ - /* basic_types */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int16 */ - /* */ - /* */ - /* A typedef for a 16bit signed integer type. */ - /* */ - typedef signed short FT_Int16; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt16 */ - /* */ - /* */ - /* A typedef for a 16bit unsigned integer type. */ - /* */ - typedef unsigned short FT_UInt16; - - /* */ - - - /* this #if 0 ... #endif clause is for documentation purposes */ -#if 0 - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int32 */ - /* */ - /* */ - /* A typedef for a 32bit signed integer type. The size depends on */ - /* the configuration. */ - /* */ - typedef signed XXX FT_Int32; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt32 */ - /* */ - /* A typedef for a 32bit unsigned integer type. The size depends on */ - /* the configuration. */ - /* */ - typedef unsigned XXX FT_UInt32; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int64 */ - /* */ - /* A typedef for a 64bit signed integer type. The size depends on */ - /* the configuration. Only defined if there is real 64bit support; */ - /* otherwise, it gets emulated with a structure (if necessary). */ - /* */ - typedef signed XXX FT_Int64; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt64 */ - /* */ - /* A typedef for a 64bit unsigned integer type. The size depends on */ - /* the configuration. Only defined if there is real 64bit support; */ - /* otherwise, it gets emulated with a structure (if necessary). */ - /* */ - typedef unsigned XXX FT_UInt64; - - /* */ - -#endif - -#if FT_SIZEOF_INT == 4 - - typedef signed int FT_Int32; - typedef unsigned int FT_UInt32; - -#elif FT_SIZEOF_LONG == 4 - - typedef signed long FT_Int32; - typedef unsigned long FT_UInt32; - -#else -#error "no 32bit type found -- please check your configuration files" -#endif - - - /* look up an integer type that is at least 32 bits */ -#if FT_SIZEOF_INT >= 4 - - typedef int FT_Fast; - typedef unsigned int FT_UFast; - -#elif FT_SIZEOF_LONG >= 4 - - typedef long FT_Fast; - typedef unsigned long FT_UFast; - -#endif - - - /* determine whether we have a 64-bit int type for platforms without */ - /* Autoconf */ -#if FT_SIZEOF_LONG == 8 - - /* FT_LONG64 must be defined if a 64-bit type is available */ -#define FT_LONG64 -#define FT_INT64 long -#define FT_UINT64 unsigned long - - /*************************************************************************/ - /* */ - /* A 64-bit data type may create compilation problems if you compile */ - /* in strict ANSI mode. To avoid them, we disable other 64-bit data */ - /* types if __STDC__ is defined. You can however ignore this rule */ - /* by defining the FT_CONFIG_OPTION_FORCE_INT64 configuration macro. */ - /* */ -#elif !defined( __STDC__ ) || defined( FT_CONFIG_OPTION_FORCE_INT64 ) - -#if defined( _MSC_VER ) && _MSC_VER >= 900 /* Visual C++ (and Intel C++) */ - - /* this compiler provides the __int64 type */ -#define FT_LONG64 -#define FT_INT64 __int64 -#define FT_UINT64 unsigned __int64 - -#elif defined( __BORLANDC__ ) /* Borland C++ */ - - /* XXXX: We should probably check the value of __BORLANDC__ in order */ - /* to test the compiler version. */ - - /* this compiler provides the __int64 type */ -#define FT_LONG64 -#define FT_INT64 __int64 -#define FT_UINT64 unsigned __int64 - -#elif defined( __WATCOMC__ ) /* Watcom C++ */ - - /* Watcom doesn't provide 64-bit data types */ - -#elif defined( __MWERKS__ ) /* Metrowerks CodeWarrior */ - -#define FT_LONG64 -#define FT_INT64 long long int -#define FT_UINT64 unsigned long long int - -#elif defined( __GNUC__ ) - - /* GCC provides the `long long' type */ -#define FT_LONG64 -#define FT_INT64 long long int -#define FT_UINT64 unsigned long long int - -#endif /* _MSC_VER */ - -#endif /* FT_SIZEOF_LONG == 8 */ - -#ifdef FT_LONG64 - typedef FT_INT64 FT_Int64; - typedef FT_UINT64 FT_UInt64; -#endif - - - /*************************************************************************/ - /* */ - /* miscellaneous */ - /* */ - /*************************************************************************/ - - -#define FT_BEGIN_STMNT do { -#define FT_END_STMNT } while ( 0 ) -#define FT_DUMMY_STMNT FT_BEGIN_STMNT FT_END_STMNT - - - /* typeof condition taken from gnulib's `intprops.h' header file */ -#if ( __GNUC__ >= 2 || \ - defined( __IBM__TYPEOF__ ) || \ - ( __SUNPRO_C >= 0x5110 && !__STDC__ ) ) -#define FT_TYPEOF( type ) (__typeof__ (type)) -#else -#define FT_TYPEOF( type ) /* empty */ -#endif - - -#ifdef FT_MAKE_OPTION_SINGLE_OBJECT - -#define FT_LOCAL( x ) static x -#define FT_LOCAL_DEF( x ) static x - -#else - -#ifdef __cplusplus -#define FT_LOCAL( x ) extern "C" x -#define FT_LOCAL_DEF( x ) extern "C" x -#else -#define FT_LOCAL( x ) extern x -#define FT_LOCAL_DEF( x ) x -#endif - -#endif /* FT_MAKE_OPTION_SINGLE_OBJECT */ - -#define FT_LOCAL_ARRAY( x ) extern const x -#define FT_LOCAL_ARRAY_DEF( x ) const x - - -#ifndef FT_BASE - -#ifdef __cplusplus -#define FT_BASE( x ) extern "C" x -#else -#define FT_BASE( x ) extern x -#endif - -#endif /* !FT_BASE */ - - -#ifndef FT_BASE_DEF - -#ifdef __cplusplus -#define FT_BASE_DEF( x ) x -#else -#define FT_BASE_DEF( x ) x -#endif - -#endif /* !FT_BASE_DEF */ - - -#ifndef FT_EXPORT - -#ifdef __cplusplus -#define FT_EXPORT( x ) extern "C" x -#else -#define FT_EXPORT( x ) extern x -#endif - -#endif /* !FT_EXPORT */ - - -#ifndef FT_EXPORT_DEF - -#ifdef __cplusplus -#define FT_EXPORT_DEF( x ) extern "C" x -#else -#define FT_EXPORT_DEF( x ) extern x -#endif - -#endif /* !FT_EXPORT_DEF */ - - -#ifndef FT_EXPORT_VAR - -#ifdef __cplusplus -#define FT_EXPORT_VAR( x ) extern "C" x -#else -#define FT_EXPORT_VAR( x ) extern x -#endif - -#endif /* !FT_EXPORT_VAR */ - - /* The following macros are needed to compile the library with a */ - /* C++ compiler and with 16bit compilers. */ - /* */ - - /* This is special. Within C++, you must specify `extern "C"' for */ - /* functions which are used via function pointers, and you also */ - /* must do that for structures which contain function pointers to */ - /* assure C linkage -- it's not possible to have (local) anonymous */ - /* functions which are accessed by (global) function pointers. */ - /* */ - /* */ - /* FT_CALLBACK_DEF is used to _define_ a callback function. */ - /* */ - /* FT_CALLBACK_TABLE is used to _declare_ a constant variable that */ - /* contains pointers to callback functions. */ - /* */ - /* FT_CALLBACK_TABLE_DEF is used to _define_ a constant variable */ - /* that contains pointers to callback functions. */ - /* */ - /* */ - /* Some 16bit compilers have to redefine these macros to insert */ - /* the infamous `_cdecl' or `__fastcall' declarations. */ - /* */ -#ifndef FT_CALLBACK_DEF -#ifdef __cplusplus -#define FT_CALLBACK_DEF( x ) extern "C" x -#else -#define FT_CALLBACK_DEF( x ) static x -#endif -#endif /* FT_CALLBACK_DEF */ - -#ifndef FT_CALLBACK_TABLE -#ifdef __cplusplus -#define FT_CALLBACK_TABLE extern "C" -#define FT_CALLBACK_TABLE_DEF extern "C" -#else -#define FT_CALLBACK_TABLE extern -#define FT_CALLBACK_TABLE_DEF /* nothing */ -#endif -#endif /* FT_CALLBACK_TABLE */ - - -FT_END_HEADER - - -#endif /* FTCONFIG_H_ */ - - -/* END */ diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in b/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in deleted file mode 100644 index 5df84c706800..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in +++ /dev/null @@ -1,886 +0,0 @@ -/***************************************************************************/ -/* */ -/* ftoption.h */ -/* */ -/* User-selectable configuration macros (specification only). */ -/* */ -/* Copyright 1996-2015 by */ -/* David Turner, Robert Wilhelm, and Werner Lemberg. */ -/* */ -/* This file is part of the FreeType project, and may only be used, */ -/* modified, and distributed under the terms of the FreeType project */ -/* license, LICENSE.TXT. By continuing to use, modify, or distribute */ -/* this file you indicate that you have read the license and */ -/* understand and accept it fully. */ -/* */ -/***************************************************************************/ - - -#ifndef FTOPTION_H_ -#define FTOPTION_H_ - - -#include - - -FT_BEGIN_HEADER - - /*************************************************************************/ - /* */ - /* USER-SELECTABLE CONFIGURATION MACROS */ - /* */ - /* This file contains the default configuration macro definitions for */ - /* a standard build of the FreeType library. There are three ways to */ - /* use this file to build project-specific versions of the library: */ - /* */ - /* - You can modify this file by hand, but this is not recommended in */ - /* cases where you would like to build several versions of the */ - /* library from a single source directory. */ - /* */ - /* - You can put a copy of this file in your build directory, more */ - /* precisely in `$BUILD/freetype/config/ftoption.h', where `$BUILD' */ - /* is the name of a directory that is included _before_ the FreeType */ - /* include path during compilation. */ - /* */ - /* The default FreeType Makefiles and Jamfiles use the build */ - /* directory `builds/' by default, but you can easily change */ - /* that for your own projects. */ - /* */ - /* - Copy the file to `$BUILD/ft2build.h' and modify it */ - /* slightly to pre-define the macro FT_CONFIG_OPTIONS_H used to */ - /* locate this file during the build. For example, */ - /* */ - /* #define FT_CONFIG_OPTIONS_H */ - /* #include */ - /* */ - /* will use `$BUILD/myftoptions.h' instead of this file for macro */ - /* definitions. */ - /* */ - /* Note also that you can similarly pre-define the macro */ - /* FT_CONFIG_MODULES_H used to locate the file listing of the modules */ - /* that are statically linked to the library at compile time. By */ - /* default, this file is . */ - /* */ - /* We highly recommend using the third method whenever possible. */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** G E N E R A L F R E E T Y P E 2 C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Uncomment the line below if you want to activate sub-pixel rendering */ - /* (a.k.a. LCD rendering, or ClearType) in this build of the library. */ - /* */ - /* Note that this feature is covered by several Microsoft patents */ - /* and should not be activated in any default build of the library. */ - /* */ - /* This macro has no impact on the FreeType API, only on its */ - /* _implementation_. For example, using FT_RENDER_MODE_LCD when calling */ - /* FT_Render_Glyph still generates a bitmap that is 3 times wider than */ - /* the original size in case this macro isn't defined; however, each */ - /* triplet of subpixels has R=G=B. */ - /* */ - /* This is done to allow FreeType clients to run unmodified, forcing */ - /* them to display normal gray-level anti-aliased glyphs. */ - /* */ -/* #define FT_CONFIG_OPTION_SUBPIXEL_RENDERING */ - - - /*************************************************************************/ - /* */ - /* Many compilers provide a non-ANSI 64-bit data type that can be used */ - /* by FreeType to speed up some computations. However, this will create */ - /* some problems when compiling the library in strict ANSI mode. */ - /* */ - /* For this reason, the use of 64-bit integers is normally disabled when */ - /* the __STDC__ macro is defined. You can however disable this by */ - /* defining the macro FT_CONFIG_OPTION_FORCE_INT64 here. */ - /* */ - /* For most compilers, this will only create compilation warnings when */ - /* building the library. */ - /* */ - /* ObNote: The compiler-specific 64-bit integers are detected in the */ - /* file `ftconfig.h' either statically or through the */ - /* `configure' script on supported platforms. */ - /* */ -#undef FT_CONFIG_OPTION_FORCE_INT64 - - - /*************************************************************************/ - /* */ - /* If this macro is defined, do not try to use an assembler version of */ - /* performance-critical functions (e.g. FT_MulFix). You should only do */ - /* that to verify that the assembler function works properly, or to */ - /* execute benchmark tests of the various implementations. */ -/* #define FT_CONFIG_OPTION_NO_ASSEMBLER */ - - - /*************************************************************************/ - /* */ - /* If this macro is defined, try to use an inlined assembler version of */ - /* the `FT_MulFix' function, which is a `hotspot' when loading and */ - /* hinting glyphs, and which should be executed as fast as possible. */ - /* */ - /* Note that if your compiler or CPU is not supported, this will default */ - /* to the standard and portable implementation found in `ftcalc.c'. */ - /* */ -#define FT_CONFIG_OPTION_INLINE_MULFIX - - - /*************************************************************************/ - /* */ - /* LZW-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `compress' program. This is mostly used to parse many of the PCF */ - /* files that come with various X11 distributions. The implementation */ - /* uses NetBSD's `zopen' to partially uncompress the file on the fly */ - /* (see src/lzw/ftgzip.c). */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#define FT_CONFIG_OPTION_USE_LZW - - - /*************************************************************************/ - /* */ - /* Gzip-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `gzip' program. This is mostly used to parse many of the PCF files */ - /* that come with XFree86. The implementation uses `zlib' to */ - /* partially uncompress the file on the fly (see src/gzip/ftgzip.c). */ - /* */ - /* Define this macro if you want to enable this `feature'. See also */ - /* the macro FT_CONFIG_OPTION_SYSTEM_ZLIB below. */ - /* */ -#define FT_CONFIG_OPTION_USE_ZLIB - - - /*************************************************************************/ - /* */ - /* ZLib library selection */ - /* */ - /* This macro is only used when FT_CONFIG_OPTION_USE_ZLIB is defined. */ - /* It allows FreeType's `ftgzip' component to link to the system's */ - /* installation of the ZLib library. This is useful on systems like */ - /* Unix or VMS where it generally is already available. */ - /* */ - /* If you let it undefined, the component will use its own copy */ - /* of the zlib sources instead. These have been modified to be */ - /* included directly within the component and *not* export external */ - /* function names. This allows you to link any program with FreeType */ - /* _and_ ZLib without linking conflicts. */ - /* */ - /* Do not #undef this macro here since the build system might define */ - /* it for certain configurations only. */ - /* */ -#mesondefine FT_CONFIG_OPTION_SYSTEM_ZLIB - - - /*************************************************************************/ - /* */ - /* Bzip2-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `bzip2' program. This is mostly used to parse many of the PCF */ - /* files that come with XFree86. The implementation uses `libbz2' to */ - /* partially uncompress the file on the fly (see src/bzip2/ftbzip2.c). */ - /* Contrary to gzip, bzip2 currently is not included and need to use */ - /* the system available bzip2 implementation. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_BZIP2 - - - /*************************************************************************/ - /* */ - /* Define to disable the use of file stream functions and types, FILE, */ - /* fopen() etc. Enables the use of smaller system libraries on embedded */ - /* systems that have multiple system libraries, some with or without */ - /* file stream support, in the cases where file stream support is not */ - /* necessary such as memory loading of font files. */ - /* */ -/* #define FT_CONFIG_OPTION_DISABLE_STREAM_SUPPORT */ - - - /*************************************************************************/ - /* */ - /* PNG bitmap support. */ - /* */ - /* FreeType now handles loading color bitmap glyphs in the PNG format. */ - /* This requires help from the external libpng library. Uncompressed */ - /* color bitmaps do not need any external libraries and will be */ - /* supported regardless of this configuration. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_PNG - - - /*************************************************************************/ - /* */ - /* HarfBuzz support. */ - /* */ - /* FreeType uses the HarfBuzz library to improve auto-hinting of */ - /* OpenType fonts. If available, many glyphs not directly addressable */ - /* by a font's character map will be hinted also. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_HARFBUZZ - - - /*************************************************************************/ - /* */ - /* DLL export compilation */ - /* */ - /* When compiling FreeType as a DLL, some systems/compilers need a */ - /* special keyword in front OR after the return type of function */ - /* declarations. */ - /* */ - /* Two macros are used within the FreeType source code to define */ - /* exported library functions: FT_EXPORT and FT_EXPORT_DEF. */ - /* */ - /* FT_EXPORT( return_type ) */ - /* */ - /* is used in a function declaration, as in */ - /* */ - /* FT_EXPORT( FT_Error ) */ - /* FT_Init_FreeType( FT_Library* alibrary ); */ - /* */ - /* */ - /* FT_EXPORT_DEF( return_type ) */ - /* */ - /* is used in a function definition, as in */ - /* */ - /* FT_EXPORT_DEF( FT_Error ) */ - /* FT_Init_FreeType( FT_Library* alibrary ) */ - /* { */ - /* ... some code ... */ - /* return FT_Err_Ok; */ - /* } */ - /* */ - /* You can provide your own implementation of FT_EXPORT and */ - /* FT_EXPORT_DEF here if you want. If you leave them undefined, they */ - /* will be later automatically defined as `extern return_type' to */ - /* allow normal compilation. */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_EXPORT(x) extern x */ -/* #define FT_EXPORT_DEF(x) x */ - - - /*************************************************************************/ - /* */ - /* Glyph Postscript Names handling */ - /* */ - /* By default, FreeType 2 is compiled with the `psnames' module. This */ - /* module is in charge of converting a glyph name string into a */ - /* Unicode value, or return a Macintosh standard glyph name for the */ - /* use with the TrueType `post' table. */ - /* */ - /* Undefine this macro if you do not want `psnames' compiled in your */ - /* build of FreeType. This has the following effects: */ - /* */ - /* - The TrueType driver will provide its own set of glyph names, */ - /* if you build it to support postscript names in the TrueType */ - /* `post' table. */ - /* */ - /* - The Type 1 driver will not be able to synthesize a Unicode */ - /* charmap out of the glyphs found in the fonts. */ - /* */ - /* You would normally undefine this configuration macro when building */ - /* a version of FreeType that doesn't contain a Type 1 or CFF driver. */ - /* */ -#define FT_CONFIG_OPTION_POSTSCRIPT_NAMES - - - /*************************************************************************/ - /* */ - /* Postscript Names to Unicode Values support */ - /* */ - /* By default, FreeType 2 is built with the `PSNames' module compiled */ - /* in. Among other things, the module is used to convert a glyph name */ - /* into a Unicode value. This is especially useful in order to */ - /* synthesize on the fly a Unicode charmap from the CFF/Type 1 driver */ - /* through a big table named the `Adobe Glyph List' (AGL). */ - /* */ - /* Undefine this macro if you do not want the Adobe Glyph List */ - /* compiled in your `PSNames' module. The Type 1 driver will not be */ - /* able to synthesize a Unicode charmap out of the glyphs found in the */ - /* fonts. */ - /* */ -#define FT_CONFIG_OPTION_ADOBE_GLYPH_LIST - - - /*************************************************************************/ - /* */ - /* Support for Mac fonts */ - /* */ - /* Define this macro if you want support for outline fonts in Mac */ - /* format (mac dfont, mac resource, macbinary containing a mac */ - /* resource) on non-Mac platforms. */ - /* */ - /* Note that the `FOND' resource isn't checked. */ - /* */ -#define FT_CONFIG_OPTION_MAC_FONTS - - - /*************************************************************************/ - /* */ - /* Guessing methods to access embedded resource forks */ - /* */ - /* Enable extra Mac fonts support on non-Mac platforms (e.g. */ - /* GNU/Linux). */ - /* */ - /* Resource forks which include fonts data are stored sometimes in */ - /* locations which users or developers don't expected. In some cases, */ - /* resource forks start with some offset from the head of a file. In */ - /* other cases, the actual resource fork is stored in file different */ - /* from what the user specifies. If this option is activated, */ - /* FreeType tries to guess whether such offsets or different file */ - /* names must be used. */ - /* */ - /* Note that normal, direct access of resource forks is controlled via */ - /* the FT_CONFIG_OPTION_MAC_FONTS option. */ - /* */ -#ifdef FT_CONFIG_OPTION_MAC_FONTS -#define FT_CONFIG_OPTION_GUESSING_EMBEDDED_RFORK -#endif - - - /*************************************************************************/ - /* */ - /* Allow the use of FT_Incremental_Interface to load typefaces that */ - /* contain no glyph data, but supply it via a callback function. */ - /* This is required by clients supporting document formats which */ - /* supply font data incrementally as the document is parsed, such */ - /* as the Ghostscript interpreter for the PostScript language. */ - /* */ -#define FT_CONFIG_OPTION_INCREMENTAL - - - /*************************************************************************/ - /* */ - /* The size in bytes of the render pool used by the scan-line converter */ - /* to do all of its work. */ - /* */ -#define FT_RENDER_POOL_SIZE 16384L - - - /*************************************************************************/ - /* */ - /* FT_MAX_MODULES */ - /* */ - /* The maximum number of modules that can be registered in a single */ - /* FreeType library object. 32 is the default. */ - /* */ -#define FT_MAX_MODULES 32 - - - /*************************************************************************/ - /* */ - /* Debug level */ - /* */ - /* FreeType can be compiled in debug or trace mode. In debug mode, */ - /* errors are reported through the `ftdebug' component. In trace */ - /* mode, additional messages are sent to the standard output during */ - /* execution. */ - /* */ - /* Define FT_DEBUG_LEVEL_ERROR to build the library in debug mode. */ - /* Define FT_DEBUG_LEVEL_TRACE to build it in trace mode. */ - /* */ - /* Don't define any of these macros to compile in `release' mode! */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_DEBUG_LEVEL_ERROR */ -/* #define FT_DEBUG_LEVEL_TRACE */ - - - /*************************************************************************/ - /* */ - /* Autofitter debugging */ - /* */ - /* If FT_DEBUG_AUTOFIT is defined, FreeType provides some means to */ - /* control the autofitter behaviour for debugging purposes with global */ - /* boolean variables (consequently, you should *never* enable this */ - /* while compiling in `release' mode): */ - /* */ - /* _af_debug_disable_horz_hints */ - /* _af_debug_disable_vert_hints */ - /* _af_debug_disable_blue_hints */ - /* */ - /* Additionally, the following functions provide dumps of various */ - /* internal autofit structures to stdout (using `printf'): */ - /* */ - /* af_glyph_hints_dump_points */ - /* af_glyph_hints_dump_segments */ - /* af_glyph_hints_dump_edges */ - /* af_glyph_hints_get_num_segments */ - /* af_glyph_hints_get_segment_offset */ - /* */ - /* As an argument, they use another global variable: */ - /* */ - /* _af_debug_hints */ - /* */ - /* Please have a look at the `ftgrid' demo program to see how those */ - /* variables and macros should be used. */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_DEBUG_AUTOFIT */ - - - /*************************************************************************/ - /* */ - /* Memory Debugging */ - /* */ - /* FreeType now comes with an integrated memory debugger that is */ - /* capable of detecting simple errors like memory leaks or double */ - /* deletes. To compile it within your build of the library, you */ - /* should define FT_DEBUG_MEMORY here. */ - /* */ - /* Note that the memory debugger is only activated at runtime when */ - /* when the _environment_ variable `FT2_DEBUG_MEMORY' is defined also! */ - /* */ - /* Do not #undef this macro here since the build system might define */ - /* it for certain configurations only. */ - /* */ -/* #define FT_DEBUG_MEMORY */ - - - /*************************************************************************/ - /* */ - /* Module errors */ - /* */ - /* If this macro is set (which is _not_ the default), the higher byte */ - /* of an error code gives the module in which the error has occurred, */ - /* while the lower byte is the real error code. */ - /* */ - /* Setting this macro makes sense for debugging purposes only, since */ - /* it would break source compatibility of certain programs that use */ - /* FreeType 2. */ - /* */ - /* More details can be found in the files ftmoderr.h and fterrors.h. */ - /* */ -#undef FT_CONFIG_OPTION_USE_MODULE_ERRORS - - - /*************************************************************************/ - /* */ - /* Position Independent Code */ - /* */ - /* If this macro is set (which is _not_ the default), FreeType2 will */ - /* avoid creating constants that require address fixups. Instead the */ - /* constants will be moved into a struct and additional intialization */ - /* code will be used. */ - /* */ - /* Setting this macro is needed for systems that prohibit address */ - /* fixups, such as BREW. */ - /* */ -#mesondefine FT_CONFIG_OPTION_PIC - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** S F N T D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_EMBEDDED_BITMAPS if you want to support */ - /* embedded bitmaps in all formats using the SFNT module (namely */ - /* TrueType & OpenType). */ - /* */ -#define TT_CONFIG_OPTION_EMBEDDED_BITMAPS - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_POSTSCRIPT_NAMES if you want to be able to */ - /* load and enumerate the glyph Postscript names in a TrueType or */ - /* OpenType file. */ - /* */ - /* Note that when you do not compile the `PSNames' module by undefining */ - /* the above FT_CONFIG_OPTION_POSTSCRIPT_NAMES, the `sfnt' module will */ - /* contain additional code used to read the PS Names table from a font. */ - /* */ - /* (By default, the module uses `PSNames' to extract glyph names.) */ - /* */ -#define TT_CONFIG_OPTION_POSTSCRIPT_NAMES - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_SFNT_NAMES if your applications need to */ - /* access the internal name table in a SFNT-based format like TrueType */ - /* or OpenType. The name table contains various strings used to */ - /* describe the font, like family name, copyright, version, etc. It */ - /* does not contain any glyph name though. */ - /* */ - /* Accessing SFNT names is done through the functions declared in */ - /* `ftsnames.h'. */ - /* */ -#define TT_CONFIG_OPTION_SFNT_NAMES - - - /*************************************************************************/ - /* */ - /* TrueType CMap support */ - /* */ - /* Here you can fine-tune which TrueType CMap table format shall be */ - /* supported. */ -#define TT_CONFIG_CMAP_FORMAT_0 -#define TT_CONFIG_CMAP_FORMAT_2 -#define TT_CONFIG_CMAP_FORMAT_4 -#define TT_CONFIG_CMAP_FORMAT_6 -#define TT_CONFIG_CMAP_FORMAT_8 -#define TT_CONFIG_CMAP_FORMAT_10 -#define TT_CONFIG_CMAP_FORMAT_12 -#define TT_CONFIG_CMAP_FORMAT_13 -#define TT_CONFIG_CMAP_FORMAT_14 - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** T R U E T Y P E D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_BYTECODE_INTERPRETER if you want to compile */ - /* a bytecode interpreter in the TrueType driver. */ - /* */ - /* By undefining this, you will only compile the code necessary to load */ - /* TrueType glyphs without hinting. */ - /* */ - /* Do not #undef this macro here, since the build system might */ - /* define it for certain configurations only. */ - /* */ -#define TT_CONFIG_OPTION_BYTECODE_INTERPRETER - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_SUBPIXEL_HINTING if you want to compile */ - /* EXPERIMENTAL subpixel hinting support into the TrueType driver. This */ - /* replaces the native TrueType hinting mechanism when anything but */ - /* FT_RENDER_MODE_MONO is requested. */ - /* */ - /* Enabling this causes the TrueType driver to ignore instructions under */ - /* certain conditions. This is done in accordance with the guide here, */ - /* with some minor differences: */ - /* */ - /* http://www.microsoft.com/typography/cleartype/truetypecleartype.aspx */ - /* */ - /* By undefining this, you only compile the code necessary to hint */ - /* TrueType glyphs with native TT hinting. */ - /* */ - /* This option requires TT_CONFIG_OPTION_BYTECODE_INTERPRETER to be */ - /* defined. */ - /* */ -/* #define TT_CONFIG_OPTION_SUBPIXEL_HINTING */ - - - /*************************************************************************/ - /* */ - /* If you define TT_CONFIG_OPTION_UNPATENTED_HINTING, a special version */ - /* of the TrueType bytecode interpreter is used that doesn't implement */ - /* any of the patented opcodes and algorithms. The patents related to */ - /* TrueType hinting have expired worldwide since May 2010; this option */ - /* is now deprecated. */ - /* */ - /* Note that the TT_CONFIG_OPTION_UNPATENTED_HINTING macro is *ignored* */ - /* if you define TT_CONFIG_OPTION_BYTECODE_INTERPRETER; in other words, */ - /* either define TT_CONFIG_OPTION_BYTECODE_INTERPRETER or */ - /* TT_CONFIG_OPTION_UNPATENTED_HINTING but not both at the same time. */ - /* */ - /* This macro is only useful for a small number of font files (mostly */ - /* for Asian scripts) that require bytecode interpretation to properly */ - /* load glyphs. For all other fonts, this produces unpleasant results, */ - /* thus the unpatented interpreter is never used to load glyphs from */ - /* TrueType fonts unless one of the following two options is used. */ - /* */ - /* - The unpatented interpreter is explicitly activated by the user */ - /* through the FT_PARAM_TAG_UNPATENTED_HINTING parameter tag */ - /* when opening the FT_Face. */ - /* */ - /* - FreeType detects that the FT_Face corresponds to one of the */ - /* `trick' fonts (e.g., `Mingliu') it knows about. The font engine */ - /* contains a hard-coded list of font names and other matching */ - /* parameters (see function `tt_face_init' in file */ - /* `src/truetype/ttobjs.c'). */ - /* */ - /* Here a sample code snippet for using FT_PARAM_TAG_UNPATENTED_HINTING. */ - /* */ - /* { */ - /* FT_Parameter parameter; */ - /* FT_Open_Args open_args; */ - /* */ - /* */ - /* parameter.tag = FT_PARAM_TAG_UNPATENTED_HINTING; */ - /* */ - /* open_args.flags = FT_OPEN_PATHNAME | FT_OPEN_PARAMS; */ - /* open_args.pathname = my_font_pathname; */ - /* open_args.num_params = 1; */ - /* open_args.params = ¶meter; */ - /* */ - /* error = FT_Open_Face( library, &open_args, index, &face ); */ - /* ... */ - /* } */ - /* */ -/* #define TT_CONFIG_OPTION_UNPATENTED_HINTING */ - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED to compile the */ - /* TrueType glyph loader to use Apple's definition of how to handle */ - /* component offsets in composite glyphs. */ - /* */ - /* Apple and MS disagree on the default behavior of component offsets */ - /* in composites. Apple says that they should be scaled by the scaling */ - /* factors in the transformation matrix (roughly, it's more complex) */ - /* while MS says they should not. OpenType defines two bits in the */ - /* composite flags array which can be used to disambiguate, but old */ - /* fonts will not have them. */ - /* */ - /* http://www.microsoft.com/typography/otspec/glyf.htm */ - /* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html */ - /* */ -#undef TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_GX_VAR_SUPPORT if you want to include */ - /* support for Apple's distortable font technology (fvar, gvar, cvar, */ - /* and avar tables). This has many similarities to Type 1 Multiple */ - /* Masters support. */ - /* */ -#define TT_CONFIG_OPTION_GX_VAR_SUPPORT - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_BDF if you want to include support for */ - /* an embedded `BDF ' table within SFNT-based bitmap formats. */ - /* */ -#define TT_CONFIG_OPTION_BDF - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** T Y P E 1 D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* T1_MAX_DICT_DEPTH is the maximum depth of nest dictionaries and */ - /* arrays in the Type 1 stream (see t1load.c). A minimum of 4 is */ - /* required. */ - /* */ -#define T1_MAX_DICT_DEPTH 5 - - - /*************************************************************************/ - /* */ - /* T1_MAX_SUBRS_CALLS details the maximum number of nested sub-routine */ - /* calls during glyph loading. */ - /* */ -#define T1_MAX_SUBRS_CALLS 16 - - - /*************************************************************************/ - /* */ - /* T1_MAX_CHARSTRING_OPERANDS is the charstring stack's capacity. A */ - /* minimum of 16 is required. */ - /* */ - /* The Chinese font MingTiEG-Medium (CNS 11643 character set) needs 256. */ - /* */ -#define T1_MAX_CHARSTRINGS_OPERANDS 256 - - - /*************************************************************************/ - /* */ - /* Define this configuration macro if you want to prevent the */ - /* compilation of `t1afm', which is in charge of reading Type 1 AFM */ - /* files into an existing face. Note that if set, the T1 driver will be */ - /* unable to produce kerning distances. */ - /* */ -#undef T1_CONFIG_OPTION_NO_AFM - - - /*************************************************************************/ - /* */ - /* Define this configuration macro if you want to prevent the */ - /* compilation of the Multiple Masters font support in the Type 1 */ - /* driver. */ - /* */ -#undef T1_CONFIG_OPTION_NO_MM_SUPPORT - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** C F F D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Using CFF_CONFIG_OPTION_DARKENING_PARAMETER_{X,Y}{1,2,3,4} it is */ - /* possible to set up the default values of the four control points that */ - /* define the stem darkening behaviour of the (new) CFF engine. For */ - /* more details please read the documentation of the */ - /* `darkening-parameters' property of the cff driver module (file */ - /* `ftcffdrv.h'), which allows the control at run-time. */ - /* */ - /* Do *not* undefine these macros! */ - /* */ -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 500 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 400 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 1000 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 275 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 1667 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 275 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 2333 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 0 - - - /*************************************************************************/ - /* */ - /* CFF_CONFIG_OPTION_OLD_ENGINE controls whether the pre-Adobe CFF */ - /* engine gets compiled into FreeType. If defined, it is possible to */ - /* switch between the two engines using the `hinting-engine' property of */ - /* the cff driver module. */ - /* */ -/* #define CFF_CONFIG_OPTION_OLD_ENGINE */ - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** A U T O F I T M O D U L E C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Compile autofit module with CJK (Chinese, Japanese, Korean) script */ - /* support. */ - /* */ -#define AF_CONFIG_OPTION_CJK - - /*************************************************************************/ - /* */ - /* Compile autofit module with Indic script support. */ - /* */ -#define AF_CONFIG_OPTION_INDIC - - /*************************************************************************/ - /* */ - /* Compile autofit module with warp hinting. The idea of the warping */ - /* code is to slightly scale and shift a glyph within a single dimension */ - /* so that as much of its segments are aligned (more or less) on the */ - /* grid. To find out the optimal scaling and shifting value, various */ - /* parameter combinations are tried and scored. */ - /* */ - /* This experimental option is active only if the rendering mode is */ - /* FT_RENDER_MODE_LIGHT; you can switch warping on and off with the */ - /* `warping' property of the auto-hinter (see file `ftautoh.h' for more */ - /* information; by default it is switched off). */ - /* */ -#define AF_CONFIG_OPTION_USE_WARPER - - /* */ - - - /* - * This macro is obsolete. Support has been removed in FreeType - * version 2.5. - */ -/* #define FT_CONFIG_OPTION_OLD_INTERNALS */ - - - /* - * This macro is defined if either unpatented or native TrueType - * hinting is requested by the definitions above. - */ -#ifdef TT_CONFIG_OPTION_BYTECODE_INTERPRETER -#define TT_USE_BYTECODE_INTERPRETER -#undef TT_CONFIG_OPTION_UNPATENTED_HINTING -#elif defined TT_CONFIG_OPTION_UNPATENTED_HINTING -#define TT_USE_BYTECODE_INTERPRETER -#endif - - - /* - * Check CFF darkening parameters. The checks are the same as in function - * `cff_property_set' in file `cffdrivr.c'. - */ -#if CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 < 0 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 < 0 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 > 500 -#error "Invalid CFF darkening parameters!" -#endif - -FT_END_HEADER - - -#endif /* FTOPTION_H_ */ - - -/* END */ diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build b/subprojects/packagefiles/freetype-2.6.1-meson/meson.build deleted file mode 100644 index 1fd4bc44e7b5..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build +++ /dev/null @@ -1,199 +0,0 @@ -project('freetype2', 'c', - version: '2.6.1', - license: '(FTL OR GPL-2.0-or-later) AND BSD-3-Clause AND MIT AND MIT-Modern-Variant AND Zlib', - license_files: [ - 'docs/LICENSE.TXT', - 'docs/FTL.TXT', - 'docs/GPLv2.TXT', - ], - meson_version: '>=1.1.0') - -# NOTE about FreeType versions -# There are 3 versions numbers associated with each releases: -# - official release number (eg. 2.6.1) - accessible via -# FREETYPE_{MAJOR,MINOR,PATCH} macros from FT_FREETYPE_H -# - libtool-specific version number, this is what is returned by -# freetype-config --version / pkg-config --modversion (eg. 22.1.16) -# - the platform-specific shared object version number (eg. 6.16.1) -# See https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/VERSIONS.TXT -# for more information -release_version = meson.project_version() -libtool_version = '18.1.12' -so_version = '6.12.1' -so_soversion = '6' - -pkgmod = import('pkgconfig') - -cc = meson.get_compiler('c') - -base_sources = [ - 'src/autofit/autofit.c', - 'src/base/ftbase.c', - 'src/base/ftbbox.c', - 'src/base/ftbdf.c', - 'src/base/ftbitmap.c', - 'src/base/ftcid.c', - 'src/base/ftfntfmt.c', - 'src/base/ftfstype.c', - 'src/base/ftgasp.c', - 'src/base/ftglyph.c', - 'src/base/ftgxval.c', - 'src/base/ftinit.c', - 'src/base/ftlcdfil.c', - 'src/base/ftmm.c', - 'src/base/ftotval.c', - 'src/base/ftpatent.c', - 'src/base/ftpfr.c', - 'src/base/ftstroke.c', - 'src/base/ftsynth.c', - 'src/base/ftsystem.c', - 'src/base/fttype1.c', - 'src/base/ftwinfnt.c', - 'src/bdf/bdf.c', - 'src/bzip2/ftbzip2.c', - 'src/cache/ftcache.c', - 'src/cff/cff.c', - 'src/cid/type1cid.c', - 'src/gzip/ftgzip.c', - 'src/lzw/ftlzw.c', - 'src/pcf/pcf.c', - 'src/pfr/pfr.c', - 'src/psaux/psaux.c', - 'src/pshinter/pshinter.c', - 'src/psnames/psnames.c', - 'src/raster/raster.c', - 'src/sfnt/sfnt.c', - 'src/smooth/smooth.c', - 'src/truetype/truetype.c', - 'src/type1/type1.c', - 'src/type42/type42.c', - 'src/winfonts/winfnt.c', -] - -ft2build_h = [ - 'include/ft2build.h', -] - -ft_headers = [ - 'include/freetype/freetype.h', - 'include/freetype/ftadvanc.h', - 'include/freetype/ftautoh.h', - 'include/freetype/ftbbox.h', - 'include/freetype/ftbdf.h', - 'include/freetype/ftbitmap.h', - 'include/freetype/ftbzip2.h', - 'include/freetype/ftcache.h', - 'include/freetype/ftcffdrv.h', - 'include/freetype/ftchapters.h', - 'include/freetype/ftcid.h', - 'include/freetype/fterrdef.h', - 'include/freetype/fterrors.h', - 'include/freetype/ftfntfmt.h', - 'include/freetype/ftgasp.h', - 'include/freetype/ftglyph.h', - 'include/freetype/ftgxval.h', - 'include/freetype/ftgzip.h', - 'include/freetype/ftimage.h', - 'include/freetype/ftincrem.h', - 'include/freetype/ftlcdfil.h', - 'include/freetype/ftlist.h', - 'include/freetype/ftlzw.h', - 'include/freetype/ftmac.h', - 'include/freetype/ftmm.h', - 'include/freetype/ftmodapi.h', - 'include/freetype/ftmoderr.h', - 'include/freetype/ftotval.h', - 'include/freetype/ftoutln.h', - 'include/freetype/ftpfr.h', - 'include/freetype/ftrender.h', - 'include/freetype/ftsizes.h', - 'include/freetype/ftsnames.h', - 'include/freetype/ftstroke.h', - 'include/freetype/ftsynth.h', - 'include/freetype/ftsystem.h', - 'include/freetype/fttrigon.h', - 'include/freetype/ftttdrv.h', - 'include/freetype/fttypes.h', - 'include/freetype/ftwinfnt.h', - 'include/freetype/t1tables.h', - 'include/freetype/ttnameid.h', - 'include/freetype/tttables.h', - 'include/freetype/tttags.h', - 'include/freetype/ttunpat.h', -] - -ft_config_headers = [ - 'include/freetype/config/ftconfig.h', - 'include/freetype/config/ftheader.h', - 'include/freetype/config/ftmodule.h', - 'include/freetype/config/ftoption.h', - 'include/freetype/config/ftstdlib.h', -] - -if host_machine.system() == 'windows' - base_sources += [ - 'builds/windows/ftdebug.c', - ] -else - base_sources += [ - 'src/base/ftdebug.c', - ] -endif - -c_args = [ - '-DFT2_BUILD_LIBRARY', - '-DFT_CONFIG_CONFIG_H=', - '-DFT_CONFIG_OPTIONS_H=' -] - -check_headers = [] - -if ['linux', 'darwin', 'cygwin'].contains(host_machine.system()) - check_headers += [ - ['unistd.h'], - ['fcntl.h'], - ['stdint.h'], - ] - ftconfig_h_in = files('builds/unix/ftconfig.h.in') -else - ftconfig_h_in = files('include/freetype/config/ftconfig.h') -endif - -conf = configuration_data() -deps = [] -incbase = include_directories(['include']) - -foreach check : check_headers - name = check[0] - - if cc.has_header(name) - conf.set('HAVE_@0@'.format(name.to_upper().underscorify()), 1) - endif -endforeach - -configure_file(input: ftconfig_h_in, - output: 'ftconfig.h', - configuration: conf) - -ft_config_headers += [configure_file(input: 'include/freetype/config/ftoption.h.in', - output: 'ftoption.h', - configuration: conf)] - -if cc.get_id() == 'emscripten' - kwargs = {} -else - kwargs = {'gnu_symbol_visibility': 'inlineshidden'} -endif - -libfreetype = static_library('freetype', base_sources, - include_directories: incbase, - dependencies: deps, - c_args: c_args, - kwargs: kwargs -) - -freetype_dep = declare_dependency( - link_with: libfreetype, - include_directories : incbase, - dependencies: deps, -) diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h b/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h deleted file mode 100644 index d88a82a2eec8..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h +++ /dev/null @@ -1,284 +0,0 @@ -/* zconf.h -- configuration of the zlib compression library - * Copyright (C) 1995-2002 Jean-loup Gailly. - * For conditions of distribution and use, see copyright notice in zlib.h - */ - -/* @(#) $Id$ */ - -#ifndef _ZCONF_H -#define _ZCONF_H - -/* - * If you *really* need a unique prefix for all types and library functions, - * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it. - */ -#ifdef Z_PREFIX -# define deflateInit_ z_deflateInit_ -# define deflate z_deflate -# define deflateEnd z_deflateEnd -# define inflateInit_ z_inflateInit_ -# define inflate z_inflate -# define inflateEnd z_inflateEnd -# define deflateInit2_ z_deflateInit2_ -# define deflateSetDictionary z_deflateSetDictionary -# define deflateCopy z_deflateCopy -# define deflateReset z_deflateReset -# define deflateParams z_deflateParams -# define inflateInit2_ z_inflateInit2_ -# define inflateSetDictionary z_inflateSetDictionary -# define inflateSync z_inflateSync -# define inflateSyncPoint z_inflateSyncPoint -# define inflateReset z_inflateReset -# define compress z_compress -# define compress2 z_compress2 -# define uncompress z_uncompress -# define adler32 z_adler32 -# define crc32 z_crc32 -# define get_crc_table z_get_crc_table - -# define Byte z_Byte -# define uInt z_uInt -# define uLong z_uLong -# define Bytef z_Bytef -# define charf z_charf -# define intf z_intf -# define uIntf z_uIntf -# define uLongf z_uLongf -# define voidpf z_voidpf -# define voidp z_voidp -#endif - -#if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) -# define WIN32 -#endif -#if defined(__GNUC__) || defined(WIN32) || defined(__386__) || defined(i386) -# ifndef __32BIT__ -# define __32BIT__ -# endif -#endif -#if defined(__MSDOS__) && !defined(MSDOS) -# define MSDOS -#endif - -/* WinCE doesn't have errno.h */ -#ifdef _WIN32_WCE -# define NO_ERRNO_H -#endif - - -/* - * Compile with -DMAXSEG_64K if the alloc function cannot allocate more - * than 64k bytes at a time (needed on systems with 16-bit int). - */ -#if defined(MSDOS) && !defined(__32BIT__) -# define MAXSEG_64K -#endif -#ifdef MSDOS -# define UNALIGNED_OK -#endif - -#if (defined(MSDOS) || defined(_WINDOWS) || defined(WIN32)) && !defined(STDC) -# define STDC -#endif -#if defined(__STDC__) || defined(__cplusplus) || defined(__OS2__) -# ifndef STDC -# define STDC -# endif -#endif - -#ifndef STDC -# ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */ -# define const -# endif -#endif - -/* Some Mac compilers merge all .h files incorrectly: */ -#if defined(__MWERKS__) || defined(applec) ||defined(THINK_C) ||defined(__SC__) -# define NO_DUMMY_DECL -#endif - -/* Old Borland C and LCC incorrectly complains about missing returns: */ -#if defined(__BORLANDC__) && (__BORLANDC__ < 0x500) -# define NEED_DUMMY_RETURN -#endif - -#if defined(__LCC__) -# define NEED_DUMMY_RETURN -#endif - -/* Maximum value for memLevel in deflateInit2 */ -#ifndef MAX_MEM_LEVEL -# ifdef MAXSEG_64K -# define MAX_MEM_LEVEL 8 -# else -# define MAX_MEM_LEVEL 9 -# endif -#endif - -/* Maximum value for windowBits in deflateInit2 and inflateInit2. - * WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files - * created by gzip. (Files created by minigzip can still be extracted by - * gzip.) - */ -#ifndef MAX_WBITS -# define MAX_WBITS 15 /* 32K LZ77 window */ -#endif - -/* The memory requirements for deflate are (in bytes): - (1 << (windowBits+2)) + (1 << (memLevel+9)) - that is: 128K for windowBits=15 + 128K for memLevel = 8 (default values) - plus a few kilobytes for small objects. For example, if you want to reduce - the default memory requirements from 256K to 128K, compile with - make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7" - Of course this will generally degrade compression (there's no free lunch). - - The memory requirements for inflate are (in bytes) 1 << windowBits - that is, 32K for windowBits=15 (default value) plus a few kilobytes - for small objects. -*/ - - /* Type declarations */ - -#ifndef OF /* function prototypes */ -# ifdef STDC -# define OF(args) args -# else -# define OF(args) () -# endif -#endif - -/* The following definitions for FAR are needed only for MSDOS mixed - * model programming (small or medium model with some far allocations). - * This was tested only with MSC; for other MSDOS compilers you may have - * to define NO_MEMCPY in zutil.h. If you don't need the mixed model, - * just define FAR to be empty. - */ -#if (defined(M_I86SM) || defined(M_I86MM)) && !defined(__32BIT__) - /* MSC small or medium model */ -# define SMALL_MEDIUM -# ifdef _MSC_VER -# define FAR _far -# else -# define FAR far -# endif -#endif -#if defined(__BORLANDC__) && (defined(__SMALL__) || defined(__MEDIUM__)) -# ifndef __32BIT__ -# define SMALL_MEDIUM -# define FAR _far -# endif -#endif - -/* Compile with -DZLIB_DLL for Windows DLL support */ -#if defined(ZLIB_DLL) -# if defined(_WINDOWS) || defined(WINDOWS) -# ifdef FAR -# undef FAR -# endif -# include -# define ZEXPORT(x) x WINAPI -# ifdef WIN32 -# define ZEXPORTVA(x) x WINAPIV -# else -# define ZEXPORTVA(x) x FAR _cdecl _export -# endif -# endif -# if defined (__BORLANDC__) -# if (__BORLANDC__ >= 0x0500) && defined (WIN32) -# include -# define ZEXPORT(x) x __declspec(dllexport) WINAPI -# define ZEXPORTRVA(x) x __declspec(dllexport) WINAPIV -# else -# if defined (_Windows) && defined (__DLL__) -# define ZEXPORT(x) x _export -# define ZEXPORTVA(x) x _export -# endif -# endif -# endif -#endif - - -#ifndef ZEXPORT -# define ZEXPORT(x) static x -#endif -#ifndef ZEXPORTVA -# define ZEXPORTVA(x) static x -#endif -#ifndef ZEXTERN -# define ZEXTERN(x) static x -#endif -#ifndef ZEXTERNDEF -# define ZEXTERNDEF(x) static x -#endif - -#ifndef FAR -# define FAR -#endif - -#if !defined(__MACTYPES__) -typedef unsigned char Byte; /* 8 bits */ -#endif -typedef unsigned int uInt; /* 16 bits or more */ -typedef unsigned long uLong; /* 32 bits or more */ - -#ifdef SMALL_MEDIUM - /* Borland C/C++ and some old MSC versions ignore FAR inside typedef */ -# define Bytef Byte FAR -#else - typedef Byte FAR Bytef; -#endif -typedef char FAR charf; -typedef int FAR intf; -typedef uInt FAR uIntf; -typedef uLong FAR uLongf; - -#ifdef STDC - typedef void FAR *voidpf; - typedef void *voidp; -#else - typedef Byte FAR *voidpf; - typedef Byte *voidp; -#endif - -#ifdef HAVE_UNISTD_H -# include /* for off_t */ -# include /* for SEEK_* and off_t */ -# define z_off_t off_t -#endif -#ifndef SEEK_SET -# define SEEK_SET 0 /* Seek from beginning of file. */ -# define SEEK_CUR 1 /* Seek from current position. */ -# define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ -#endif -#ifndef z_off_t -# define z_off_t long -#endif - -/* MVS linker does not support external names larger than 8 bytes */ -#if defined(__MVS__) -# pragma map(deflateInit_,"DEIN") -# pragma map(deflateInit2_,"DEIN2") -# pragma map(deflateEnd,"DEEND") -# pragma map(inflateInit_,"ININ") -# pragma map(inflateInit2_,"ININ2") -# pragma map(inflateEnd,"INEND") -# pragma map(inflateSync,"INSY") -# pragma map(inflateSetDictionary,"INSEDI") -# pragma map(inflate_blocks,"INBL") -# pragma map(inflate_blocks_new,"INBLNE") -# pragma map(inflate_blocks_free,"INBLFR") -# pragma map(inflate_blocks_reset,"INBLRE") -# pragma map(inflate_codes_free,"INCOFR") -# pragma map(inflate_codes,"INCO") -# pragma map(inflate_fast,"INFA") -# pragma map(inflate_flush,"INFLU") -# pragma map(inflate_mask,"INMA") -# pragma map(inflate_set_dictionary,"INSEDI2") -# pragma map(inflate_copyright,"INCOPY") -# pragma map(inflate_trees_bits,"INTRBI") -# pragma map(inflate_trees_dynamic,"INTRDY") -# pragma map(inflate_trees_fixed,"INTRFI") -# pragma map(inflate_trees_free,"INTRFR") -#endif - -#endif /* _ZCONF_H */ diff --git a/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch b/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch new file mode 100644 index 000000000000..5e9a6b7f9ed5 --- /dev/null +++ b/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch @@ -0,0 +1,11 @@ +--- a/meson.build 2025-03-26 03:32:12.444735795 -0400 ++++ b/meson.build 2025-03-26 03:32:16.117435140 -0400 +@@ -45,8 +45,7 @@ + if not freetype.found() + freetype = dependency( + 'freetype2', + version: '>= @0@'.format(freetype_version[0]), +- method: 'pkg-config', + fallback: ['freetype2', 'freetype_dep'], + default_options: [ + 'png=disabled', diff --git a/subprojects/sheenbidi.wrap b/subprojects/sheenbidi.wrap new file mode 100644 index 000000000000..c58277d47499 --- /dev/null +++ b/subprojects/sheenbidi.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = SheenBidi-2.9.0 +source_url = https://github.com/Tehreer/SheenBidi/archive/refs/tags/v2.9.0/sheenbidi-2.9.0.tar.gz +source_filename = sheenbidi-2.9.0.tar.gz +source_hash = e90ae142c6fc8b94366f3526f84b349a2c10137f87093db402fe51f6eace6d13