From 771fd37f1a82ea997a3c1a39b381333ea5bf650f Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sat, 29 May 2021 23:32:02 +0530 Subject: [PATCH 1/4] Rename packing to scripts --- .github/workflows/build.yml | 12 ++++----- .github/workflows/tests.yml | 19 ++++--------- {packing => scripts}/LICENSE.bin | 0 {packing => scripts}/build_pango_mac.sh | 28 ++++++++++---------- {packing => scripts}/build_pango_tests.sh | 10 +++---- {packing => scripts}/build_pkgconfig.ps1 | 0 {packing => scripts}/download_and_extract.py | 0 {packing => scripts}/download_dlls.py | 0 {packing => scripts}/inject-dlls.py | 0 {packing => scripts}/test_wheels.sh | 0 10 files changed, 30 insertions(+), 39 deletions(-) rename {packing => scripts}/LICENSE.bin (100%) rename {packing => scripts}/build_pango_mac.sh (85%) rename {packing => scripts}/build_pango_tests.sh (88%) rename {packing => scripts}/build_pkgconfig.ps1 (100%) rename {packing => scripts}/download_and_extract.py (100%) rename {packing => scripts}/download_dlls.py (100%) rename {packing => scripts}/inject-dlls.py (100%) rename {packing => scripts}/test_wheels.sh (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f16f5ddb0..401b1e505 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,13 +33,13 @@ jobs: env: CIBW_BUILD: cp36-${{ matrix.platform_id }} cp37-${{ matrix.platform_id }} cp38-${{ matrix.platform_id }} cp39-${{ matrix.platform_id }} CIBW_SKIP: pp* cp35 - CIBW_BEFORE_BUILD_MACOS: "source packing/build_pango_mac.sh && pip install cython && cd manimpango && cythonize cmanimpango.pyx -3 -k -f && cd ../ && pip install . && pkg-config --libs pango" - CIBW_BEFORE_BUILD_WINDOWS: "pip install cython && python packing/download_dlls.py && cd manimpango && cythonize cmanimpango.pyx -3 -k -f && cd ../ && pkg-config --libs pango && pip install ." + CIBW_BEFORE_BUILD_MACOS: "source scripts/build_pango_mac.sh && pip install cython && cd manimpango && cythonize cmanimpango.pyx -3 -k -f && cd ../ && pip install . && pkg-config --libs pango" + CIBW_BEFORE_BUILD_WINDOWS: "pip install cython && python scripts/download_dlls.py && cd manimpango && cythonize cmanimpango.pyx -3 -k -f && cd ../ && pkg-config --libs pango && pip install ." CIBW_ENVIRONMENT_WINDOWS: "PKG_CONFIG_PATH='C:\\cibw\\vendor\\lib\\pkgconfig'" CIBW_ENVIRONMENT_MACOS: "PKG_CONFIG_PATH='/Users/runner/pangobuild/lib/pkgconfig'" - CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python packing/inject-dlls.py {wheel} {dest_dir} C:\cibw\vendor\bin + CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dlls.py {wheel} {dest_dir} C:\cibw\vendor\bin CIBW_TEST_REQUIRES: pytest Cython pytest-cov - CIBW_TEST_COMMAND: "bash {project}/packing/test_wheels.sh {project}" + CIBW_TEST_COMMAND: "bash {project}/scripts/test_wheels.sh {project}" steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -62,14 +62,14 @@ jobs: $ErrorActionPreference = 'Stop' $env:PATH="$env:PATH;C:\cibw\pkg-config\bin" $env:PKG_CONFIG_PATH="C:\cibw\vendor\lib\pkgconfig" - Copy-Item packing/LICENSE.bin . + Copy-Item scripts/LICENSE.bin . Rename-Item LICENSE.bin LICENSE.win32 python -m cibuildwheel --output-dir wheelhouse - name: Build wheels (Non-Windows) if: runner.os != 'windows' run: | - cp packing/LICENSE.bin . + cp scripts/LICENSE.bin . python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35e3bbb25..4c5cf27b2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,11 +37,11 @@ jobs: uses: actions/cache@v2 with: path: ~/pangoprefix - key: ${{ hashFiles('packing/build_pango_tests.sh') }}-${{ runner.os }} + key: ${{ hashFiles('scripts/build_pango_tests.sh') }}-${{ runner.os }} - name: Install System Dependency if: steps.cache-pango.outputs.cache-hit != 'true' run: | - source packing/build_pango_tests.sh + source scripts/build_pango_tests.sh - name: Install python dependencies run: | @@ -50,8 +50,6 @@ jobs: - name: Run Tests run: | python setup.py build_ext -i --coverage - python setup.py sdist - pip install . pytest - name: Coverage run: | @@ -99,10 +97,7 @@ jobs: - name: Run tests shell: msys2 {0} run: | - pip install . python setup.py build_ext -i --coverage - python setup.py sdist - python -m pip install dist/* pytest - name: Coverage shell: msys2 {0} @@ -136,10 +131,10 @@ jobs: uses: actions/cache@v2 with: path: C:\cibw\pkg-config - key: ${{ hashFiles('packing/download_dlls.py') }}-${{ hashFiles('packing/build_pkgconfig.ps1') }}-1 + key: ${{ hashFiles('scripts/download_dlls.py') }}-${{ hashFiles('scripts/build_pkgconfig.ps1') }}-1 - name: Download Binary run: | - python packing/download_dlls.py + python scripts/download_dlls.py - name: Set Path run: | $env:Path = "C:\cibw\pkg-config\bin;C:\cibw\vendor\bin;$($env:PATH)" @@ -149,8 +144,6 @@ jobs: $env:PKG_CONFIG_PATH="C:\cibw\vendor\lib\pkgconfig" pip install -r requirements-dev.txt python setup.py build_ext -i --coverage - python setup.py sdist - python -m pip install dist/* $env:PATH="C:\cibw\vendor\bin;$env:PATH" pytest - name: Coverage @@ -165,15 +158,13 @@ jobs: architecture: "x86" - name: Download Binary run: | - python packing/download_dlls.py + python scripts/download_dlls.py - name: Build x86 Build run: | $env:PATH="$env:PATH;C:\cibw\vendor\pkg-config\bin;C:\cibw\vendor\bin" $env:PKG_CONFIG_PATH="C:\cibw\vendor\lib\pkgconfig" pip install -r requirements-dev.txt python setup.py build_ext -i --coverage - python setup.py sdist - python -m pip install dist/* $env:PATH="C:\cibw\vendor\bin;$env:PATH" pytest - name: Coverage diff --git a/packing/LICENSE.bin b/scripts/LICENSE.bin similarity index 100% rename from packing/LICENSE.bin rename to scripts/LICENSE.bin diff --git a/packing/build_pango_mac.sh b/scripts/build_pango_mac.sh similarity index 85% rename from packing/build_pango_mac.sh rename to scripts/build_pango_mac.sh index 29dcdc92e..86ad7ba6c 100644 --- a/packing/build_pango_mac.sh +++ b/scripts/build_pango_mac.sh @@ -32,20 +32,20 @@ cd pango echo "::group::Downloading Files" python -m pip install requests -python $FILE_PATH/packing/download_and_extract.py "http://download.gnome.org/sources/pango/${PANGO_VERSION%.*}/pango-${PANGO_VERSION}.tar.xz" pango -python $FILE_PATH/packing/download_and_extract.py "http://download.gnome.org/sources/glib/${GLIB_VERSION%.*}/glib-${GLIB_VERSION}.tar.xz" glib -python $FILE_PATH/packing/download_and_extract.py "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VERSION}/fribidi-${FRIBIDI_VERSION}.tar.xz" fribidi -python $FILE_PATH/packing/download_and_extract.py "https://cairographics.org/snapshots/cairo-${CAIRO_VERSION}.tar.xz" cairo -python $FILE_PATH/packing/download_and_extract.py "https://cairographics.org/releases/pixman-${PIXMAN_VERSION}.tar.gz" pixman -python $FILE_PATH/packing/download_and_extract.py "https://www.freedesktop.org/software/fontconfig/release/fontconfig-${FONTCONFIG_VERSION}.tar.xz" fontconfig -python $FILE_PATH/packing/download_and_extract.py "https://downloads.sourceforge.net/project/freetype/freetype2/${FREETYPE_VERSION}/freetype-${FREETYPE_VERSION}.tar.gz" freetype -#python $FILE_PATH/packing/download_and_extract.py "https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz" freetype -python $FILE_PATH/packing/download_and_extract.py "https://github.com/libexpat/libexpat/releases/download/R_2_2_10/expat-2.2.10.tar.xz" expat -python $FILE_PATH/packing/download_and_extract.py "https://mirrors.kernel.org/gnu/gperf/gperf-${GPERF_VERSION}.tar.gz" gperf -python $FILE_PATH/packing/download_and_extract.py "https://downloads.sourceforge.net/project/libpng/libpng16/${LIBPNG_VERSION}/libpng-${LIBPNG_VERSION}.tar.xz" libpng -python $FILE_PATH/packing/download_and_extract.py "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz" harfbuzz -python $FILE_PATH/packing/download_and_extract.py "https://zlib.net/fossils/zlib-${ZLIB_VERSION}.tar.gz" zlib -python $FILE_PATH/packing/download_and_extract.py "https://ftp.pcre.org/pub/pcre/pcre-${PCRE_VERSION}.tar.bz2" pcre +python $FILE_PATH/scripts/download_and_extract.py "http://download.gnome.org/sources/pango/${PANGO_VERSION%.*}/pango-${PANGO_VERSION}.tar.xz" pango +python $FILE_PATH/scripts/download_and_extract.py "http://download.gnome.org/sources/glib/${GLIB_VERSION%.*}/glib-${GLIB_VERSION}.tar.xz" glib +python $FILE_PATH/scripts/download_and_extract.py "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VERSION}/fribidi-${FRIBIDI_VERSION}.tar.xz" fribidi +python $FILE_PATH/scripts/download_and_extract.py "https://cairographics.org/snapshots/cairo-${CAIRO_VERSION}.tar.xz" cairo +python $FILE_PATH/scripts/download_and_extract.py "https://cairographics.org/releases/pixman-${PIXMAN_VERSION}.tar.gz" pixman +python $FILE_PATH/scripts/download_and_extract.py "https://www.freedesktop.org/software/fontconfig/release/fontconfig-${FONTCONFIG_VERSION}.tar.xz" fontconfig +python $FILE_PATH/scripts/download_and_extract.py "https://downloads.sourceforge.net/project/freetype/freetype2/${FREETYPE_VERSION}/freetype-${FREETYPE_VERSION}.tar.gz" freetype +#python $FILE_PATH/scripts/download_and_extract.py "https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz" freetype +python $FILE_PATH/scripts/download_and_extract.py "https://github.com/libexpat/libexpat/releases/download/R_2_2_10/expat-2.2.10.tar.xz" expat +python $FILE_PATH/scripts/download_and_extract.py "https://mirrors.kernel.org/gnu/gperf/gperf-${GPERF_VERSION}.tar.gz" gperf +python $FILE_PATH/scripts/download_and_extract.py "https://downloads.sourceforge.net/project/libpng/libpng16/${LIBPNG_VERSION}/libpng-${LIBPNG_VERSION}.tar.xz" libpng +python $FILE_PATH/scripts/download_and_extract.py "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz" harfbuzz +python $FILE_PATH/scripts/download_and_extract.py "https://zlib.net/fossils/zlib-${ZLIB_VERSION}.tar.gz" zlib +python $FILE_PATH/scripts/download_and_extract.py "https://ftp.pcre.org/pub/pcre/pcre-${PCRE_VERSION}.tar.bz2" pcre curl -L "https://github.com/frida/proxy-libintl/archive/0.1.tar.gz" -o 0.1.tar.gz tar -xf 0.1.tar.gz mv proxy-libintl-0.1 proxy-libintl diff --git a/packing/build_pango_tests.sh b/scripts/build_pango_tests.sh similarity index 88% rename from packing/build_pango_tests.sh rename to scripts/build_pango_tests.sh index 0ac725031..06731730e 100644 --- a/packing/build_pango_tests.sh +++ b/scripts/build_pango_tests.sh @@ -19,11 +19,11 @@ cd pango echo "::group::Downloading Files" python -m pip install requests -python $FILE_PATH/packing/download_and_extract.py "http://download.gnome.org/sources/pango/${PANGO_VERSION%.*}/pango-${PANGO_VERSION}.tar.xz" pango -python $FILE_PATH/packing/download_and_extract.py "http://download.gnome.org/sources/glib/${GLIB_VERSION%.*}/glib-${GLIB_VERSION}.tar.xz" glib -python $FILE_PATH/packing/download_and_extract.py "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VERSION}/fribidi-${FRIBIDI_VERSION}.tar.xz" fribidi -python $FILE_PATH/packing/download_and_extract.py "https://gitlab.freedesktop.org/cairo/cairo/-/archive/${CAIRO_VERSION}/cairo-${CAIRO_VERSION}.tar.gz" cairo -python $FILE_PATH/packing/download_and_extract.py "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz" harfbuzz +python $FILE_PATH/scripts/download_and_extract.py "http://download.gnome.org/sources/pango/${PANGO_VERSION%.*}/pango-${PANGO_VERSION}.tar.xz" pango +python $FILE_PATH/scripts/download_and_extract.py "http://download.gnome.org/sources/glib/${GLIB_VERSION%.*}/glib-${GLIB_VERSION}.tar.xz" glib +python $FILE_PATH/scripts/download_and_extract.py "https://github.com/fribidi/fribidi/releases/download/v${FRIBIDI_VERSION}/fribidi-${FRIBIDI_VERSION}.tar.xz" fribidi +python $FILE_PATH/scripts/download_and_extract.py "https://gitlab.freedesktop.org/cairo/cairo/-/archive/${CAIRO_VERSION}/cairo-${CAIRO_VERSION}.tar.gz" cairo +python $FILE_PATH/scripts/download_and_extract.py "https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz" harfbuzz python -m pip uninstall -y requests diff --git a/packing/build_pkgconfig.ps1 b/scripts/build_pkgconfig.ps1 similarity index 100% rename from packing/build_pkgconfig.ps1 rename to scripts/build_pkgconfig.ps1 diff --git a/packing/download_and_extract.py b/scripts/download_and_extract.py similarity index 100% rename from packing/download_and_extract.py rename to scripts/download_and_extract.py diff --git a/packing/download_dlls.py b/scripts/download_dlls.py similarity index 100% rename from packing/download_dlls.py rename to scripts/download_dlls.py diff --git a/packing/inject-dlls.py b/scripts/inject-dlls.py similarity index 100% rename from packing/inject-dlls.py rename to scripts/inject-dlls.py diff --git a/packing/test_wheels.sh b/scripts/test_wheels.sh similarity index 100% rename from packing/test_wheels.sh rename to scripts/test_wheels.sh From 37d94b656e2415dc7a703359a2facdf5229eff61 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sat, 29 May 2021 23:34:20 +0530 Subject: [PATCH 2/4] Stable API Change the whole API into something simple --- manimpango/__init__.py | 24 +- manimpango/_distributor_init.py | 21 ++ manimpango/bufferproxy/__init__.pxd | 0 manimpango/bufferproxy/bufferproxy.pxd | 19 ++ manimpango/bufferproxy/bufferproxy.pyx | 93 ++++++ manimpango/cmanimpango.pyx | 297 ------------------ manimpango/exceptions.py | 7 + manimpango/font_manager/__init__.py | 236 ++++++++++++++ .../_register_font.pxd} | 0 manimpango/font_manager/_register_font.pyi | 4 + .../_register_font.pyx} | 9 + manimpango/{ => includes}/cairo.pxd | 2 + manimpango/{ => includes}/glib.pxd | 5 +- manimpango/{ => includes}/pango.pxd | 74 +++++ manimpango/layout/__init__.pxd | 1 + manimpango/layout/__init__.py | 103 ++++++ .../{cmanimpango.pxd => layout/utils.pxd} | 3 +- manimpango/layout/utils.pyi | 1 + manimpango/layout/utils.pyx | 19 ++ manimpango/py.typed | 0 manimpango/renderer/__init__.pxd | 0 manimpango/renderer/__init__.py | 2 + manimpango/renderer/renderer.pxd | 27 ++ manimpango/renderer/renderer.pyx | 201 ++++++++++++ manimpango/utils/__init__.pxd | 0 manimpango/utils/__init__.py | 6 + manimpango/utils/_cutils.pyx | 12 + manimpango/utils/_list_fonts.pyi | 3 + manimpango/utils/_list_fonts.pyx | 42 +++ manimpango/utils/colours.pxd | 10 + manimpango/utils/colours.pyi | 15 + manimpango/utils/colours.pyx | 109 +++++++ manimpango/utils/enums.pyi | 6 + manimpango/{ => utils}/enums.pyx | 5 + manimpango/{ => utils}/utils.py | 0 setup.py | 55 +++- 36 files changed, 1091 insertions(+), 320 deletions(-) create mode 100644 manimpango/_distributor_init.py create mode 100644 manimpango/bufferproxy/__init__.pxd create mode 100644 manimpango/bufferproxy/bufferproxy.pxd create mode 100644 manimpango/bufferproxy/bufferproxy.pyx delete mode 100644 manimpango/cmanimpango.pyx create mode 100644 manimpango/exceptions.py create mode 100644 manimpango/font_manager/__init__.py rename manimpango/{register_font.pxd => font_manager/_register_font.pxd} (100%) create mode 100644 manimpango/font_manager/_register_font.pyi rename manimpango/{register_font.pyx => font_manager/_register_font.pyx} (96%) rename manimpango/{ => includes}/cairo.pxd (92%) rename manimpango/{ => includes}/glib.pxd (89%) rename manimpango/{ => includes}/pango.pxd (69%) create mode 100644 manimpango/layout/__init__.pxd create mode 100644 manimpango/layout/__init__.py rename manimpango/{cmanimpango.pxd => layout/utils.pxd} (52%) create mode 100644 manimpango/layout/utils.pyi create mode 100644 manimpango/layout/utils.pyx create mode 100644 manimpango/py.typed create mode 100644 manimpango/renderer/__init__.pxd create mode 100644 manimpango/renderer/__init__.py create mode 100644 manimpango/renderer/renderer.pxd create mode 100644 manimpango/renderer/renderer.pyx create mode 100644 manimpango/utils/__init__.pxd create mode 100644 manimpango/utils/__init__.py create mode 100644 manimpango/utils/_cutils.pyx create mode 100644 manimpango/utils/_list_fonts.pyi create mode 100644 manimpango/utils/_list_fonts.pyx create mode 100644 manimpango/utils/colours.pxd create mode 100644 manimpango/utils/colours.pyi create mode 100644 manimpango/utils/colours.pyx create mode 100644 manimpango/utils/enums.pyi rename manimpango/{ => utils}/enums.pyx (93%) rename manimpango/{ => utils}/utils.py (100%) diff --git a/manimpango/__init__.py b/manimpango/__init__.py index 6221d071e..d15726c1c 100644 --- a/manimpango/__init__.py +++ b/manimpango/__init__.py @@ -1,24 +1,24 @@ # -*- coding: utf-8 -*- -import os import sys +from . import _distributor_init # noqa: F401 from ._version import __version__ # noqa: F403,F401 -if os.name == "nt": # pragma: no cover - os.environ["PATH"] = ( - f"{os.path.abspath(os.path.dirname(__file__))}" - f"{os.pathsep}" - f"{os.environ['PATH']}" - ) try: - from .cmanimpango import * # noqa: F403,F401 - from .enums import * # noqa: F403,F401 - from .register_font import * # noqa: F403,F401 + from .utils import * # noqa: F403,F401 + + initialize_glib() # noqa: F405 + + # utils should be imported first + from .font_manager import * # noqa: F403,F401 + from .layout import * # noqa: F403,F401 + from .renderer import * # noqa: F403,F401 + + except ImportError as ie: # pragma: no cover py_ver = ".".join(map(str, sys.version_info[:3])) msg = f""" - -ManimPango could not import and load the necessary shared libraries. +ManimPango could not import and initialize itself. This error may occur when ManimPango and its dependencies are improperly set up. Please make sure the following versions are what you expect: diff --git a/manimpango/_distributor_init.py b/manimpango/_distributor_init.py new file mode 100644 index 000000000..0d7dcf407 --- /dev/null +++ b/manimpango/_distributor_init.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +Helper to preload windows dlls to prevent dll not found errors. +Once a DLL is preloaded, its namespace is made available to any +subsequent DLL. +""" +import glob +import os + +if os.name == "nt": # pragma: no cover + try: + from ctypes import WinDLL + + basedir = os.path.dirname(__file__) + except: # noqa: E722 + pass + else: + for filename in glob.glob(os.path.join(basedir, "*.dll")): + WinDLL(os.path.abspath(filename)) + if hasattr(os, "add_dll_directory"): + os.add_dll_directory(basedir) diff --git a/manimpango/bufferproxy/__init__.pxd b/manimpango/bufferproxy/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/manimpango/bufferproxy/bufferproxy.pxd b/manimpango/bufferproxy/bufferproxy.pxd new file mode 100644 index 000000000..c2c77353e --- /dev/null +++ b/manimpango/bufferproxy/bufferproxy.pxd @@ -0,0 +1,19 @@ +cdef class BaseBuffer: + cdef size_t _buffer_size(self) + cdef void* _buffer_ptr(self) + cdef bint _buffer_writable(self) + + +cdef class ImageBuffer(Buffer): + cdef int size + cdef unsigned char *buf + cdef cairo_t* cr + cdef cairo_surface_t* surface + cdef int set_cairo_data( + self, + cairo_t* cr, + cairo_surface_t* surface + ) + cdef size_t _buffer_size(self) + cdef void* _buffer_ptr(self) + cdef bint _buffer_writable(self) diff --git a/manimpango/bufferproxy/bufferproxy.pyx b/manimpango/bufferproxy/bufferproxy.pyx new file mode 100644 index 000000000..875cb81a5 --- /dev/null +++ b/manimpango/bufferproxy/bufferproxy.pyx @@ -0,0 +1,93 @@ +# This should contain the buffer proxy to get the data +# from cairo to say a numpy array. + +# Have a look at how I previouslt implemented this +# https://github.com/naveen521kk/text2svg/blob/feat-numpy/text2svg/buf.pyx +# https://github.com/naveen521kk/text2svg/blob/feat-numpy/text2svg/ctext2np.pyx + + +from libc.string cimport memcpy +from cpython cimport PyBuffer_FillInfo, PyBUF_WRITABLE + +cdef class BaseBuffer: + """A base for Buffer which can be read using Pillow + or converted to an array using Numpy. + + Note + ==== + The buffer or array isn't writable. + So, you should make a copy of the buffer to + write. + """ + cdef size_t _buffer_size(self): + return 0 + + cdef void* _buffer_ptr(self): + return NULL + + cdef bint _buffer_writable(self): + return True + + def __getbuffer__(self, Py_buffer *view, int flags): + if flags & PyBUF_WRITABLE and not self._buffer_writable(): + raise ValueError('buffer is not writable') + PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) + + @property + def buffer_size(self): + """The size of the buffer in bytes.""" + return self._buffer_size() + + @property + def buffer_ptr(self): + """The memory address of the buffer.""" + return self._buffer_ptr() + + def to_bytes(self): + """Return the contents of this buffer as ``bytes``. + """ + return (self._buffer_ptr())[:self._buffer_size()] + +cdef class ImageBuffer(Buffer): + # This will give the ImageBuffer from Cairo. + # The Cairo's surface shouldn't be destroyed + # before reading this or else it would create + # SegFaults. Now, we don't plan to depend on + # Numpy so we are going to get a reference of + # the Cairo's surface and free it up only when + # this buffer is garbage collected. + cdef int size + cdef unsigned char *buf + cdef cairo_t* cr + cdef cairo_surface_t* surface + + def __init__(self): + pass + + def __dealloc__(self): + # destroy the surface and context we have + cairo_destroy(self.cr) + cairo_surface_destroy(self.surface) + + cdef int set_cairo_data( + self, + cairo_t* cr, + cairo_surface_t* surface + ): + cdef int height + cdef int stride + self.cr = cr + self.surface = surface + self.buf = cairo_image_surface_get_data (surface) + height = cairo_image_surface_get_height (surface) + stride = cairo_image_surface_get_stride (surface) + self.size = height * stride + + cdef size_t _buffer_size(self): + return self.size + + cdef void * _buffer_ptr(self): + return self.buf + + cdef bint _buffer_writable(self): + return False diff --git a/manimpango/cmanimpango.pyx b/manimpango/cmanimpango.pyx deleted file mode 100644 index e7952b57b..000000000 --- a/manimpango/cmanimpango.pyx +++ /dev/null @@ -1,297 +0,0 @@ -from xml.sax.saxutils import escape -from .utils import * -from .enums import Alignment -import warnings -import typing - -class TextSetting: - """Formatting for slices of a :class:`manim.mobject.svg.text_mobject.Text` object.""" - def __init__( - self, - start:int, - end:int, - font:str, - slant, - weight, - line_num=-1 - ): - self.start = start - self.end = end - self.font = font.encode('utf-8') - self.slant = slant - self.weight = weight - self.line_num = line_num - -def text2svg( - settings:list, - size:int, - line_spacing:int, - disable_liga:bool, - file_name:str, - START_X:int, - START_Y:int, - width:int, - height:int, - orig_text:str, - pango_width: typing.Union[int, None] = None, -) -> int: - """Render an SVG file from a :class:`manim.mobject.svg.text_mobject.Text` object.""" - cdef cairo_surface_t* surface - cdef cairo_t* cr - cdef PangoFontDescription* font_desc - cdef PangoLayout* layout - cdef double font_size_c = size - cdef cairo_status_t status - cdef int temp_width - - file_name_bytes = file_name.encode("utf-8") - surface = cairo_svg_surface_create(file_name_bytes,width,height) - - if surface == NULL: - raise MemoryError("Cairo.SVGSurface can't be created.") - - cr = cairo_create(surface) - status = cairo_status(cr) - - if cr == NULL or status == CAIRO_STATUS_NO_MEMORY: - cairo_destroy(cr) - cairo_surface_destroy(surface) - raise MemoryError("Cairo.Context can't be created.") - elif status != CAIRO_STATUS_SUCCESS: - cairo_destroy(cr) - cairo_surface_destroy(surface) - raise Exception(cairo_status_to_string(status)) - - cairo_move_to(cr,START_X,START_Y) - offset_x = 0 - last_line_num = 0 - - layout = pango_cairo_create_layout(cr) - - if layout == NULL: - cairo_destroy(cr) - cairo_surface_destroy(surface) - raise MemoryError("Pango.Layout can't be created from Cairo Context.") - - if pango_width is None: - pango_layout_set_width(layout, pango_units_from_double(width)) - else: - pango_layout_set_width(layout, pango_units_from_double(pango_width)) - - for setting in settings: - family = setting.font - style = PangoUtils.str2style(setting.slant) - weight = PangoUtils.str2weight(setting.weight) - text_str = orig_text[setting.start : setting.end].replace("\n", " ") - text = text_str.encode('utf-8') - font_desc = pango_font_description_new() - if font_desc==NULL: - cairo_destroy(cr) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise MemoryError("Pango.FontDesc can't be created.") - pango_font_description_set_size(font_desc, pango_units_from_double(font_size_c)) - if family: - pango_font_description_set_family(font_desc, family) - pango_font_description_set_style(font_desc, style.value) - pango_font_description_set_weight(font_desc, weight.value) - pango_layout_set_font_description(layout, font_desc) - pango_font_description_free(font_desc) - if setting.line_num != last_line_num: - offset_x = 0 - last_line_num = setting.line_num - cairo_move_to(cr,START_X + offset_x,START_Y + line_spacing * setting.line_num) - - pango_cairo_update_layout(cr,layout) - if disable_liga: - text_bytes = escape(text.decode('utf-8')) - markup = f"{text_bytes}" - markup_bytes = markup.encode('utf-8') - pango_layout_set_markup(layout, markup_bytes, -1) - else: - pango_layout_set_text(layout,text,-1) - pango_cairo_show_layout(cr, layout) - pango_layout_get_size(layout,&temp_width,NULL) - offset_x += pango_units_to_double(temp_width) - - status = cairo_status(cr) - - if cr == NULL or status == CAIRO_STATUS_NO_MEMORY: - cairo_destroy(cr) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise MemoryError("Cairo.Context can't be created.") - elif status != CAIRO_STATUS_SUCCESS: - cairo_destroy(cr) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise Exception(cairo_status_to_string(status).decode()) - - cairo_destroy(cr) - cairo_surface_destroy(surface) - g_object_unref(layout) - return file_name - -class MarkupUtils: - @staticmethod - def validate(markup: str) -> str: - """Validates whether markup is a valid Markup - and return the error's if any. - - Parameters - ========== - markup : :class:`str` - The markup which should be checked. - - Returns - ======= - :class:`str` - Returns empty string if markup is valid. If markup - contains error it return the error message. - - """ - cdef GError *err = NULL - text_bytes = markup.encode("utf-8") - res = pango_parse_markup( - text_bytes, - -1, - 0, - NULL, - NULL, - NULL, - &err - ) - if res: - return "" - else: - message = err.message - g_error_free(err) - return message.decode('utf-8') - - @staticmethod - def text2svg( - text: str, - font: str, - slant: str, - weight: str, - size: int, - _: int, # for some there was a keyword here. - disable_liga: bool, - file_name: str, - START_X: int, - START_Y: int, - width: int, - height: int, - *, # keyword only arguments below - justify: bool = None, - indent: float = None, - line_spacing: float = None, - alignment: Alignment = None, - pango_width: typing.Union[int, None] = None, - ) -> int: - """Render an SVG file from a :class:`manim.mobject.svg.text_mobject.MarkupText` object.""" - cdef cairo_surface_t* surface - cdef cairo_t* context - cdef PangoFontDescription* font_desc - cdef PangoLayout* layout - cdef cairo_status_t status - cdef double font_size = size - cdef int temp_int # a temporary C integer for conversion - - file_name_bytes = file_name.encode("utf-8") - - if disable_liga: - text_bytes = f"{text}".encode("utf-8") - else: - text_bytes = text.encode("utf-8") - - surface = cairo_svg_surface_create(file_name_bytes,width,height) - if surface == NULL: - raise MemoryError("Cairo.SVGSurface can't be created.") - context = cairo_create(surface) - status = cairo_status(context) - if context == NULL or status == CAIRO_STATUS_NO_MEMORY: - cairo_destroy(context) - cairo_surface_destroy(surface) - raise MemoryError("Cairo.Context can't be created.") - elif status != CAIRO_STATUS_SUCCESS: - cairo_destroy(context) - cairo_surface_destroy(surface) - raise Exception(cairo_status_to_string(status)) - - cairo_move_to(context,START_X,START_Y) - layout = pango_cairo_create_layout(context) - if layout == NULL: - cairo_destroy(context) - cairo_surface_destroy(surface) - raise MemoryError("Pango.Layout can't be created from Cairo Context.") - - if pango_width is None: - pango_layout_set_width(layout, pango_units_from_double(width)) - else: - pango_layout_set_width(layout, pango_units_from_double(pango_width)) - - if justify: - pango_layout_set_justify(layout, justify) - - if indent: - temp_int = pango_units_from_double(indent) - pango_layout_set_indent(layout, temp_int) - - if line_spacing: - # Typical values are: 0, 1, 1.5, 2. - ret = set_line_width(layout, line_spacing) - if not ret: - # warn that line spacing don't work - # because of old Pango version they - # have - warnings.warn( - "Pango Version<1.44 found." - "Impossible to set line_spacing." - "Expect Ugly Output." - ) - - if alignment: - pango_layout_set_alignment(layout, alignment.value) - - font_desc = pango_font_description_new() - if font_desc==NULL: - cairo_destroy(context) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise MemoryError("Pango.FontDesc can't be created.") - pango_font_description_set_size(font_desc, pango_units_from_double(font_size)) - if font is not None and len(font) != 0: - pango_font_description_set_family(font_desc, font.encode("utf-8")) - pango_font_description_set_style(font_desc, PangoUtils.str2style(slant).value) - pango_font_description_set_weight(font_desc, PangoUtils.str2weight(weight).value) - pango_layout_set_font_description(layout, font_desc) - pango_font_description_free(font_desc) - - cairo_move_to(context,START_X,START_Y) - pango_cairo_update_layout(context,layout) - pango_layout_set_markup(layout,text_bytes,-1) - pango_cairo_show_layout(context, layout) - - status = cairo_status(context) - if context == NULL or status == CAIRO_STATUS_NO_MEMORY: - cairo_destroy(context) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise MemoryError("Cairo.Context can't be created.") - elif status != CAIRO_STATUS_SUCCESS: - cairo_destroy(context) - cairo_surface_destroy(surface) - g_object_unref(layout) - raise Exception(cairo_status_to_string(status).decode()) - - cairo_destroy(context) - cairo_surface_destroy(surface) - g_object_unref(layout) - return file_name - -cpdef str pango_version(): - return pango_version_string().decode('utf-8') - -cpdef str cairo_version(): - return cairo_version_string().decode('utf-8') diff --git a/manimpango/exceptions.py b/manimpango/exceptions.py new file mode 100644 index 000000000..dc940fad4 --- /dev/null +++ b/manimpango/exceptions.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +class CairoException(Exception): + pass + + +class MarkupParseError(Exception): + pass diff --git a/manimpango/font_manager/__init__.py b/manimpango/font_manager/__init__.py new file mode 100644 index 000000000..7672c99c8 --- /dev/null +++ b/manimpango/font_manager/__init__.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- + +import errno +import typing +from pathlib import Path + +import attr + +from .. import Style, Variant, Weight, list_fonts +from ._register_font import ( + fc_register_font, + fc_unregister_font, + register_font, + unregister_font, +) + +__all__ = ["FontProperties", "RegisterFont"] + + +@attr.s +class FontProperties: + """A :class:`FontProperties` are used for specifying the characteristics + of a font to load. + + Attributes + ---------- + family + The font family. + size + Size of the text. Size should not be zero. + style : :class:`Style` + Style of the text. + variant : :class:`Variant` + Variant of the text. + weight : :class:`Weight` + Weight of the text. + + Parameters + ---------- + family + The font family. + size + Size of the text. Size should not be zero. + style : :class:`Style` + Style of the text. + variant : :class:`Variant` + Variant of the text. + weight : :class:`Weight` + Weight of the text. + + + Raises + ------ + ValueError + When the :attr:`size` is set as zero. + """ + + family: typing.Optional[str] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(str)), + default=None, + ) + size: typing.Optional[float] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of((float, int))), + default=None, + ) + style: typing.Optional[Style] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(Style)), + default=None, + ) + variant: typing.Optional[Variant] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(Variant)), + default=None, + ) + weight: typing.Optional[Weight] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(Weight)), + default=None, + ) + # stretch + # gravity + # variations + + @size.validator + def check_size(self, attribute, value): + """Check whether the :attr:`size` isn't zero.""" + if value == 0: + raise ValueError("Size shouldn't be Zero.") + + # def from_string(self): + # pass + # def to_string(self): + # pass + + +@attr.s(frozen=True) +class RegisterFont(object): + """A :class:`RegisterFont` object contains utilities + to temporily add a font file to Pango's search Path. + + .. note:: + + This is a frozen object, which means you can't change it's + attributes after creating it. + + There are two methods to achive this, one using Fontconfig + and other using native backend. This depends on which backend + Pango is using. By default, Pango has these backends according to + platforms, + + + | win32 | win32 backend | + + | macOS | coretext backend | + + | fontconfig | fontconfig backend | + + + The backends can be changed using an Environment variable called + ``PANGOCAIRO_BACKEND``. For example, for using fontconfig backend on + Windows you can set:: + + PANGOCAIRO_BACKEND=fc + + and after that fontconfig backend is used. After that you can set + :attr:`use_fontconfig` to ``True`` which will add to fontconfig search + path. + + .. warning:: + + 1. Linux has only fontconfig backend and changing to anything + else would result in a crash. + 2. On Windows or macOS fontconfig backend can be problematic as + it wouldn't be able to find it's configuration files in places + it searches. This would result it no fonts situation. + + Attributes + ---------- + font_file + The path of the font file. Can be absolute or relative. + use_fontconfig + Whether to use fontconfig API. + calculate_family + Calculating the family can sometimes can be slow especially + with huge number of fonts. You can disable it by setting + :attr:`calculate_family` to False. + family + This is automatically set when :attr:`calculate_family` is ``True``. + Or else it is None. + + Parameters + ---------- + font_file + The path of the font file. Can be absolute or relative. + use_fontconfig + Whether to use fontconfig API. + calculate_family + Calculating the family can sometimes can be slow especially + with huge number of fonts. You can disable it by setting + :attr:`calculate_family` to False. + family + This is automatically set when :attr:`calculate_family` is ``True``. + Or else it is None. + + Raises + ------ + FileNotFoundError + When :attr:`font_file` is not found. + RuntimeError + This is raised when unable to register the font. + """ + + font_file: typing.Union[str, Path] = attr.ib( + validator=[attr.validators.instance_of((str, Path))], + ) + use_fontconfig: bool = attr.ib( + validator=[attr.validators.instance_of(bool)], default=False, repr=False + ) + calculate_family: bool = attr.ib( + validator=[attr.validators.instance_of(bool)], default=True, repr=False + ) + family: typing.Optional[str] = attr.ib(default=None) + + @font_file.validator + def check_font_file(self, attribute, value): + """Check whether :attr:`font_file` exists + and is a file. + """ + if not Path(value).exists(): + raise FileNotFoundError( + errno.ENOENT, + f"{value} doesn't exists.", + ) + if not Path(value).is_file(): + raise FileNotFoundError( + errno.ENOENT, + f"{value} isn't a valid file", + ) + + def __attrs_post_init__(self): + if self.calculate_family: + intial = list_fonts(self.use_fontconfig) + font_file = self.font_file + if self.use_fontconfig: + status = fc_register_font(str(font_file)) + if not status: + raise RuntimeError( + f"Can't register font file {font_file}. " + "Maybe it's an invalid file ?" + ) + else: + status = register_font(str(font_file)) + if not status: + raise RuntimeError( + f"Can't register font file {font_file}. " + "Maybe it's an invalid file ?" + ) + if self.calculate_family: + final = list_fonts(self.use_fontconfig) + family = list(set(final) - set(intial)) or None + super().__setattr__("family", family) + else: + super().__setattr__("family", None) + + def unregister(self) -> None: + """Unregister the previous registered font. + After this is called the font is removed + from the Pango's search path. + + Returns + ------- + bool + True when sucessfully unregistered or else false. + """ + if self.use_fontconfig: + return fc_unregister_font(str(self.font_file)) + else: + return unregister_font(str(self.font_file)) diff --git a/manimpango/register_font.pxd b/manimpango/font_manager/_register_font.pxd similarity index 100% rename from manimpango/register_font.pxd rename to manimpango/font_manager/_register_font.pxd diff --git a/manimpango/font_manager/_register_font.pyi b/manimpango/font_manager/_register_font.pyi new file mode 100644 index 000000000..9b92fc832 --- /dev/null +++ b/manimpango/font_manager/_register_font.pyi @@ -0,0 +1,4 @@ +def fc_register_font(font_path: str) -> bool: ... +def fc_unregister_font(font_path: str) -> bool: ... +def register_font(font_path: str) -> bool: ... +def unregister_font(font_path: str) -> bool: ... diff --git a/manimpango/register_font.pyx b/manimpango/font_manager/_register_font.pyx similarity index 96% rename from manimpango/register_font.pyx rename to manimpango/font_manager/_register_font.pyx index e4a35b232..b599d7c72 100644 --- a/manimpango/register_font.pyx +++ b/manimpango/font_manager/_register_font.pyx @@ -1,3 +1,12 @@ +# Register_Font should provide a Python Wrapper +# by returning the family name once adding it +# to search Path. Other than that the font_path +# should accept `PathLike` objects for example +# `pathlib.Path` instead of just strings. +# Other than that this can be used directly in +# our Stable API. + + from pathlib import Path from pango cimport * import copy diff --git a/manimpango/cairo.pxd b/manimpango/includes/cairo.pxd similarity index 92% rename from manimpango/cairo.pxd rename to manimpango/includes/cairo.pxd index a5de06d9a..70127f97f 100644 --- a/manimpango/cairo.pxd +++ b/manimpango/includes/cairo.pxd @@ -6,6 +6,8 @@ cdef extern from "cairo.h": ctypedef enum cairo_status_t: CAIRO_STATUS_SUCCESS CAIRO_STATUS_NO_MEMORY + ctypedef enum cairo_font_type_t: + CAIRO_FONT_TYPE_FT cairo_t* cairo_create(cairo_surface_t* target) void cairo_move_to( cairo_t* cr, diff --git a/manimpango/glib.pxd b/manimpango/includes/glib.pxd similarity index 89% rename from manimpango/glib.pxd rename to manimpango/includes/glib.pxd index 3955577d7..e28693a34 100644 --- a/manimpango/glib.pxd +++ b/manimpango/includes/glib.pxd @@ -5,9 +5,10 @@ cdef extern from "glib.h": ctypedef gint gboolean ctypedef unsigned short guint16 ctypedef char gchar + void g_object_unref(gpointer object) + void g_free(gpointer mem) + void g_set_prgname(const gchar *prgname) ctypedef struct GError: gint code gchar *message void g_error_free (GError *error) - void g_object_unref(gpointer object) - void g_free(gpointer mem) diff --git a/manimpango/pango.pxd b/manimpango/includes/pango.pxd similarity index 69% rename from manimpango/pango.pxd rename to manimpango/includes/pango.pxd index 0ee5651a4..0ee1d0e0d 100644 --- a/manimpango/pango.pxd +++ b/manimpango/includes/pango.pxd @@ -41,6 +41,10 @@ cdef extern from "pango/pangocairo.h": PANGO_ALIGN_LEFT PANGO_ALIGN_CENTER PANGO_ALIGN_RIGHT + ctypedef struct PangoColor: + guint16 red + guint16 green + guint16 blue PangoLayout* pango_cairo_create_layout(cairo_t* cr) void pango_cairo_show_layout( cairo_t* cr, @@ -137,6 +141,76 @@ cdef extern from "pango/pangocairo.h": PangoLayout *layout, PangoAlignment alignment ) + bint pango_color_parse( + PangoColor *color, + const char *spec + ) + gchar* pango_color_to_string( + const PangoColor *color + ) + PangoLayout* pango_layout_copy( + PangoLayout* src + ) + PangoAlignment pango_layout_get_alignment( + PangoLayout* layout + ) + void pango_layout_set_alignment( + PangoLayout* layout, + PangoAlignment alignment + ) + gboolean pango_layout_get_auto_dir( + PangoLayout* layout + ) + void pango_layout_set_auto_dir( + PangoLayout* layout, + gboolean auto_dir + ) + int pango_layout_get_width( + PangoLayout* layout + ) + gboolean pango_font_description_equal( + const PangoFontDescription* desc1, + const PangoFontDescription* desc2 + ) + PangoFontDescription* pango_font_description_copy( + const PangoFontDescription* desc + ) + const char* pango_font_description_get_family( + const PangoFontDescription* desc + ) + # PangoGravity pango_font_description_get_gravity ( + # const PangoFontDescription* desc + # ) + gint pango_font_description_get_size( + const PangoFontDescription* desc + ) + gboolean pango_font_description_get_size_is_absolute( + const PangoFontDescription* desc + ) + const char* pango_layout_get_text( + PangoLayout* layout + ) + PangoFontMap* pango_cairo_font_map_new_for_font_type( + cairo_font_type_t fonttype + ) + void pango_layout_set_height( + PangoLayout* layout, + int height + ) + void pango_layout_set_auto_dir( + PangoLayout* layout, + gboolean auto_dir + ) + void pango_layout_set_spacing( + PangoLayout* layout, + int spacing + ) + void pango_layout_set_line_spacing( + PangoLayout* layout, + float factor + ) + + cdef extern from *: """ diff --git a/manimpango/layout/__init__.pxd b/manimpango/layout/__init__.pxd new file mode 100644 index 000000000..6ea85b6c5 --- /dev/null +++ b/manimpango/layout/__init__.pxd @@ -0,0 +1 @@ +from .utils cimport * diff --git a/manimpango/layout/__init__.py b/manimpango/layout/__init__.py new file mode 100644 index 000000000..243c6299c --- /dev/null +++ b/manimpango/layout/__init__.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +import typing + +import attr + +from ..exceptions import MarkupParseError +from ..font_manager import FontProperties +from ..utils import Alignment +from .utils import validate_markup + +__all__ = ["Layout", "validate_markup"] + + +@attr.s +class Layout: + text: typing.Optional[str] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(str)), + default=None, + ) + width: typing.Optional[int] = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(int), + ), + ) + height: typing.Optional[int] = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(int), + ), + ) + alignment: Alignment = attr.ib( + default=Alignment.CENTER, + validator=[attr.validators.instance_of(Alignment)], + ) + auto_dir: bool = attr.ib( + default=True, + validator=[attr.validators.instance_of(bool)], + ) + markup: typing.Optional[str] = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(str), + ), + ) + indent: int = attr.ib( + default=0, + validator=attr.validators.optional( + attr.validators.instance_of(int), + ), + ) + spacing: int = attr.ib( + default=0, + validator=attr.validators.optional( + attr.validators.instance_of(int), + ), + ) + line_spacing: float = attr.ib( + default=0, + validator=attr.validators.optional( + attr.validators.instance_of((float, int)), + ), + ) + justify: bool = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(bool), + ), + ) + font_properties: FontProperties = attr.ib( + default=None, + validator=attr.validators.optional( + attr.validators.instance_of(FontProperties), + ), + ) + # single_paragraph_mode + # tab_length :int = attr.ib( + # default=8, + # validator=[ + # attr.validators.instance_of(int), + # ], + # ) + # direction + # wrap + # ellipsize + # attributes = attr.ib(default=None) + # font_description = attr.ib(default=None) + + @markup.validator + def check_markup(self, attribute, value): + if value is not None: + check = validate_markup(value) + if check: + raise MarkupParseError(check) + + def __attrs_post_init__(self): + if self.markup is None and self.text is None: + raise ValueError("Either of Markup or Text is required.") + + def __len__(self): + return len(self.text) if self.text is not None else len(self.markup) + + # def is_wrapped diff --git a/manimpango/cmanimpango.pxd b/manimpango/layout/utils.pxd similarity index 52% rename from manimpango/cmanimpango.pxd rename to manimpango/layout/utils.pxd index 5d2e25b56..dd4ba0e12 100644 --- a/manimpango/cmanimpango.pxd +++ b/manimpango/layout/utils.pxd @@ -1,3 +1,4 @@ from glib cimport * -from cairo cimport * from pango cimport * + +cpdef str validate_markup(str text) diff --git a/manimpango/layout/utils.pyi b/manimpango/layout/utils.pyi new file mode 100644 index 000000000..31f391317 --- /dev/null +++ b/manimpango/layout/utils.pyi @@ -0,0 +1 @@ +def validate_markup(text: str) -> str: ... diff --git a/manimpango/layout/utils.pyx b/manimpango/layout/utils.pyx new file mode 100644 index 000000000..5941f5b59 --- /dev/null +++ b/manimpango/layout/utils.pyx @@ -0,0 +1,19 @@ + +cpdef str validate_markup(str text): + cdef GError *err = NULL + text_bytes = text.encode("utf-8") + res = pango_parse_markup( + text_bytes, + -1, + 0, + NULL, + NULL, + NULL, + &err + ) + if res: + return "" + else: + message = err.message + g_error_free(err) + return message.decode('utf-8') diff --git a/manimpango/py.typed b/manimpango/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/manimpango/renderer/__init__.pxd b/manimpango/renderer/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/manimpango/renderer/__init__.py b/manimpango/renderer/__init__.py new file mode 100644 index 000000000..72f1b97ce --- /dev/null +++ b/manimpango/renderer/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .renderer import * # noqa: F403,F401 diff --git a/manimpango/renderer/renderer.pxd b/manimpango/renderer/renderer.pxd new file mode 100644 index 000000000..fee658cb7 --- /dev/null +++ b/manimpango/renderer/renderer.pxd @@ -0,0 +1,27 @@ +from cairo cimport * +from glib cimport * +from pango cimport * + +from ..layout import Layout +from ..font_manager import FontProperties + +cdef class CairoRenderer: + cdef object py_layout + cdef PangoLayout* layout + cdef cairo_surface_t* surface + cdef cairo_t* context + cdef PangoFontDescription* font_desc + cdef void* intialise_renderer(self) + cdef void* start_rendering(self) + cdef void* finalise_renderer(self) + cpdef bint render(self) + cdef str is_context_fine(self, raise_error=*) + cdef void* convert_py_font_to_pango_font(self) + cdef void* convert_py_layout_to_pango_layout(self) + + +cdef class SVGRenderer(CairoRenderer): + cdef str file_name + cdef float width + cdef float height + cdef object move_to diff --git a/manimpango/renderer/renderer.pyx b/manimpango/renderer/renderer.pyx new file mode 100644 index 000000000..08577fbda --- /dev/null +++ b/manimpango/renderer/renderer.pyx @@ -0,0 +1,201 @@ +import typing as T +from ..exceptions import CairoException +from ..layout import Layout +cdef class CairoRenderer: + # This class should contain things + # should provide API to render to an SVG + # or get the buffer. + def __init__(self): + pass + cdef void* intialise_renderer(self): + pass + cdef void* start_rendering(self): + self.convert_py_font_to_pango_font() + self.convert_py_layout_to_pango_layout() + + cdef void* finalise_renderer(self): + pass + cpdef bint render(self): + self.intialise_renderer() + self.start_rendering() + self.finalise_renderer() + return True + + cdef str is_context_fine(self, raise_error=True): + cdef cairo_status_t status + status = cairo_status(self.context) + if status == CAIRO_STATUS_NO_MEMORY: + cairo_destroy(self.context) + cairo_surface_destroy(self.surface) + g_object_unref(self.layout) + raise MemoryError("Cairo isn't finding memory") + elif status != CAIRO_STATUS_SUCCESS: + temp_bytes = cairo_status_to_string(status) + if raise_error: + raise CairoException(temp_bytes.decode('utf-8')) + return temp_bytes.decode('utf-8') + return "" + + cdef void* convert_py_font_to_pango_font(self): + py_fontdesc = self.py_layout.font_properties + cdef PangoFontDescription* font_desc = pango_font_description_new() + if not py_fontdesc: + return NULL + if py_fontdesc.family: + pango_font_description_set_family( + font_desc, + py_fontdesc.family.encode('utf-8'), + ) + if py_fontdesc.size: + pango_font_description_set_size( + font_desc, + pango_units_from_double(py_fontdesc.size), + ) + if py_fontdesc.style: + pango_font_description_set_style( + font_desc, + py_fontdesc.style.value, + ) + if py_fontdesc.variant: + pango_font_description_set_variant( + font_desc, + py_fontdesc.variant.value + ) + if py_fontdesc.weight: + pango_font_description_set_weight( + font_desc, + py_fontdesc.weight.value + ) + self.font_desc = font_desc + + cdef void* convert_py_layout_to_pango_layout(self): + py_layout = self.py_layout + layout = self.layout + if py_layout.text: + pango_layout_set_text( + layout, + py_layout.text.encode('utf-8'), + -1, + ) + if py_layout.width: + pango_layout_set_width( + layout, + pango_units_from_double(py_layout.width), + ) + if py_layout.height: + if py_layout.height > 0: + pango_layout_set_height( + layout, + pango_units_from_double(py_layout.height), + ) + else: + pango_layout_set_height( + layout, + py_layout.height, + ) + if py_layout.alignment: + pango_layout_set_alignment( + layout, + py_layout.alignment.value, + ) + if py_layout.auto_dir: + pango_layout_set_auto_dir( + layout, + py_layout.auto_dir, + ) + if py_layout.markup: + pango_layout_set_markup( + layout, + py_layout.markup.encode('utf-8'), + -1 + ) + if py_layout.indent: + pango_layout_set_indent( + layout, + pango_units_from_double(py_layout.indent) + ) + if py_layout.spacing: + pango_layout_set_spacing( + layout, + pango_units_from_double(py_layout.spacing) + ) + if py_layout.line_spacing: + pango_layout_set_line_spacing( + layout, + py_layout.line_spacing + ) + if py_layout.justify: + pango_layout_set_justify( + layout, + py_layout.justify + ) + +cdef class SVGRenderer(CairoRenderer): + def __init__(self, file_name: str, width:int, height:int, layout: Layout, move_to: T.Tuple[int,int] = (0,0)): + self.file_name = file_name + self.width = width + self.height = height + self.move_to = move_to + self.py_layout = layout + + cdef void* intialise_renderer(self): + move_to = self.move_to + file_name = self.file_name + width = self.width + height = self.height + surface = cairo_svg_surface_create( + file_name.encode("utf-8"), + width, + height + ) + if surface == NULL: + raise MemoryError("Cairo.SVGSurface can't be created.") + context = cairo_create(surface) + + # Now set the created things as attributes. + self.surface = surface + self.context = context + + self.is_context_fine() + + cairo_move_to(context, move_to[0], move_to[1]) + + # Create a Pango layout. + layout = pango_cairo_create_layout(context) + if layout==NULL: + cairo_destroy(context) + cairo_surface_destroy(surface) + raise MemoryError("Pango.Layout can't be created from Cairo Context.") + self.layout = layout + + cdef void* start_rendering(self): + self.convert_py_font_to_pango_font() + self.convert_py_layout_to_pango_layout() + cdef cairo_t* context + cdef cairo_surface_t* surface + cdef PangoLayout* layout + # check whether cairo is happy till now + # else error out or it may create SegFaults. + self.is_context_fine() + + context = self.context + surface = self.surface + layout = self.layout + py_layout = self.layout + + + pango_cairo_show_layout(context, layout) + + # check for status again + self.is_context_fine() + + cdef void* finalise_renderer(self): + g_object_unref(self.layout) + cairo_destroy(self.context) + cairo_surface_destroy(self.surface) + + def __copy__(self): + raise NotImplementedError + + def __deepcopy__(self): + raise NotImplementedError diff --git a/manimpango/utils/__init__.pxd b/manimpango/utils/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/manimpango/utils/__init__.py b/manimpango/utils/__init__.py new file mode 100644 index 000000000..e2c72e861 --- /dev/null +++ b/manimpango/utils/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from ._cutils import * # noqa: F403,F401 +from ._list_fonts import list_fonts # noqa: F403,F401 +from .colours import * # noqa: F403,F401 +from .enums import * # noqa: F403,F401 +from .utils import * # noqa: F403,F401 diff --git a/manimpango/utils/_cutils.pyx b/manimpango/utils/_cutils.pyx new file mode 100644 index 000000000..efee98a18 --- /dev/null +++ b/manimpango/utils/_cutils.pyx @@ -0,0 +1,12 @@ +from pango cimport * +from cairo cimport * + +cpdef int initialize_glib(): + g_set_prgname('ManimPango') + return 1 + +cpdef str pango_version(): + return pango_version_string().decode('utf-8') + +cpdef str cairo_version(): + return cairo_version_string().decode('utf-8') diff --git a/manimpango/utils/_list_fonts.pyi b/manimpango/utils/_list_fonts.pyi new file mode 100644 index 000000000..0707a8168 --- /dev/null +++ b/manimpango/utils/_list_fonts.pyi @@ -0,0 +1,3 @@ +import typing + +def list_fonts() -> typing.List[str]: ... diff --git a/manimpango/utils/_list_fonts.pyx b/manimpango/utils/_list_fonts.pyx new file mode 100644 index 000000000..3ec12050a --- /dev/null +++ b/manimpango/utils/_list_fonts.pyx @@ -0,0 +1,42 @@ +from pango cimport * +from cairo cimport * + +cpdef list list_fonts(fontconfig=False): + """Lists the fonts available to Pango. + This is usually same as system fonts but it also + includes the fonts added through :func:`register_font`. + + Returns + ------- + + :class:`list` : + List of fonts sorted alphabetically. + """ + cdef PangoFontMap* fontmap + if fontconfig: + fontmap = pango_cairo_font_map_new_for_font_type(CAIRO_FONT_TYPE_FT) + else: + fontmap = pango_cairo_font_map_new() + if fontmap is NULL: + raise MemoryError("Pango.FontMap can't be created.") + cdef int n_families = 0 + cdef PangoFontFamily** families = NULL + pango_font_map_list_families( + fontmap, + &families, + &n_families + ) + if families is NULL or n_families==0: + raise MemoryError("Pango returned unexpected length for families.") + family_list=[] + for i in range(n_families): + name = pango_font_family_get_name(families[i]) + # according to pango's docs, the `char *` returned from + # `pango_font_family_get_name`is owned by pango, and python + # shouldn't interfere with it. So, rather we are making a + # deepcopy so that we don't worry about it. + family_list.append(name.decode('utf-8')) + g_free(families) + g_object_unref(fontmap) + family_list.sort() + return family_list diff --git a/manimpango/utils/colours.pxd b/manimpango/utils/colours.pxd new file mode 100644 index 000000000..23dd16b02 --- /dev/null +++ b/manimpango/utils/colours.pxd @@ -0,0 +1,10 @@ +from pango cimport * + +cdef class Color: + cdef int red + cdef int green + cdef int blue + cdef PangoColor* color + cpdef Color copy(self) + cpdef void parse_color(self, char* spec) + cpdef str to_string(self) diff --git a/manimpango/utils/colours.pyi b/manimpango/utils/colours.pyi new file mode 100644 index 000000000..85da36f12 --- /dev/null +++ b/manimpango/utils/colours.pyi @@ -0,0 +1,15 @@ +class Color: + def __init__(self, red: int, green: int, blue: int) -> None: ... + def copy(self) -> Color: ... + @property + def red(self) -> int: ... + @red.setter + def red(self, red: int) -> None: ... + @property + def green(self) -> int: ... + @green.setter + def green(self, green: int) -> None: ... + @property + def blue(self) -> int: ... + @blue.setter + def blue(self, blue: int) -> None: ... diff --git a/manimpango/utils/colours.pyx b/manimpango/utils/colours.pyx new file mode 100644 index 000000000..cc6817168 --- /dev/null +++ b/manimpango/utils/colours.pyx @@ -0,0 +1,109 @@ +# This module contains implementation of Colours as how +# it is Pango. Should be helpful when we implement +# attributes. + +import copy + +cdef class Color: + def __init__(self, red: int, green: int, blue: int): + self.red = red + self.green = green + self.blue = blue + + cpdef Color copy(self): + """ + Make a deep copy of the ``Color`` structure. + :return: + a copy of :class:`Color` + """ + temp = Color( + red = self.red, + green = self.green, + blue = self.blue + ) + return temp + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, memo): + return self.copy() + + @property + def red(self): + return self._red + + @red.setter + def red(self, red: int): + self._red = int(red) + self.color.red = red + + @property + def green(self): + return self._green + + @green.setter + def green(self, green: int): + self._green = int(green) + self.color.green = green + + @property + def blue(self): + return self._blue + + @blue.setter + def blue(self, blue: int): + self._blue = int(blue) + self.color.blue = blue + + cpdef void parse_color(self, char* spec): + """ + Fill in the fields of a color from a string + specification. The string can either one of + a large set of standard names. (Taken from + the CSS specification), or it can be a + hexadecimal value in the form '#rgb' + '#rrggbb' '#rrrgggbbb' or '#rrrrggggbbbb' + where 'r', 'g' and 'b' are hex digits of + the red, green, and blue components of the + color, respectively. (White in the four forms + is '#fff' '#ffffff' '#fffffffff' and + '#ffffffffffff') + :param spec: + a string specifying the new color + :return: + ``TRUE`` if parsing of the specifier succeeded, + otherwise false. + """ + spec_encode = spec.encode('utf-8') + ret = pango_color_parse(self.color, spec_encode) + if ret: + self._red = int(self.color.red) + self._green = int(self.color.green) + self._blue = int(self.color.blue) + + cpdef str to_string(self): + """ + Returns a textual specification of color in the + hexadecimal form #rrrrggggbbbb, where r, g and b + are hex digits representing the red, green, and + blue components respectively. + :return: + string hexadecimal + """ + string = pango_color_to_string(self.color) + new = copy.deepcopy(string.decode("utf-8")) + g_free(string) + return new + + def __eq__(self, col) -> bool: + if isinstance(col, Color): + return ( + self.red == col.red + and self.green == col.green + and self.red == self.red + ) + raise NotImplementedError + + def __repr__(self) -> str: + return f"" diff --git a/manimpango/utils/enums.pyi b/manimpango/utils/enums.pyi new file mode 100644 index 000000000..0ecadc98e --- /dev/null +++ b/manimpango/utils/enums.pyi @@ -0,0 +1,6 @@ +from enum import Enum + +class Style(Enum): ... +class Weight(Enum): ... +class Variant(Enum): ... +class Alignment(Enum): ... diff --git a/manimpango/enums.pyx b/manimpango/utils/enums.pyx similarity index 93% rename from manimpango/enums.pyx rename to manimpango/utils/enums.pyx index 93f006980..edea58b24 100644 --- a/manimpango/enums.pyx +++ b/manimpango/utils/enums.pyx @@ -1,3 +1,8 @@ +# Enums should be Python and not here. +# This would simplify the documenting process. +# Just export the things from here and add it +# to another Python file. + from enum import Enum from pango cimport * class Style(Enum): diff --git a/manimpango/utils.py b/manimpango/utils/utils.py similarity index 100% rename from manimpango/utils.py rename to manimpango/utils/utils.py diff --git a/setup.py b/setup.py index 7abdc5cf8..58635f679 100644 --- a/setup.py +++ b/setup.py @@ -213,18 +213,49 @@ def update_dict(dict1: dict, dict2: dict): ext_modules = [ Extension( - "manimpango.cmanimpango", - [str(base_file / ("cmanimpango" + ext))], + "manimpango.utils.enums", + [str(base_file / "utils" / ("enums" + ext))], **returns, ), Extension( - "manimpango.enums", - [str(base_file / ("enums" + ext))], + "manimpango.font_manager._register_font", + [str(base_file / "font_manager" / ("_register_font" + ext))], **returns, ), + # New one's here Extension( - "manimpango.register_font", - [str(base_file / ("register_font" + ext))], + "manimpango.utils._cutils", + [str(base_file / "utils" / ("_cutils" + ext))], + **returns, + ), + Extension( + "manimpango.utils.colours", + [str(base_file / "utils" / ("colours" + ext))], + **returns, + ), + Extension( + "manimpango.renderer.renderer", + [str(base_file / "renderer" / ("renderer" + ext))], + **returns, + ), + # Extension( + # "manimpango.layout.layout", + # [str(base_file / "layout" / ("layout" + ext))], + # **returns, + # ), + # Extension( + # "manimpango.font_manager.font_description", + # [str(base_file / "font_manager" / ("font_description" + ext))], + # **returns, + # ), + Extension( + "manimpango.layout.utils", + [str(base_file / "layout" / ("utils" + ext))], + **returns, + ), + Extension( + "manimpango.utils._list_fonts", + [str(base_file / "utils" / ("_list_fonts" + ext))], **returns, ), ] @@ -232,7 +263,7 @@ def update_dict(dict1: dict, dict2: dict): ext_modules = cythonize( ext_modules, language_level=3, - include_path=["manimpango"], + include_path=[str(i) for i in Path("manimpango").iterdir() if i.is_dir()], gdb_debug=DEBUG, compiler_directives={"linetrace": coverage}, ) @@ -250,7 +281,14 @@ def update_dict(dict1: dict, dict2: dict): long_description=long_description, zip_safe=False, long_description_content_type="text/markdown", - packages=["manimpango"], + packages=[ + "manimpango", + "manimpango.font_manager", + "manimpango.includes", + "manimpango.layout", + "manimpango.renderer", + "manimpango.utils", + ], python_requires=">=3.6", platforms=["Linux", "macOS", "Windows"], keywords=["cython", "pango", "cairo", "manim"], @@ -276,4 +314,5 @@ def update_dict(dict1: dict, dict2: dict): package_data={ "manimpango": ["*.pxd", "*.pyx"], }, + install_requires=["attrs>=20.0"], ) From 94d730ab5bbcc500164b3507f5818de070492098 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sat, 29 May 2021 23:35:14 +0530 Subject: [PATCH 3/4] Fix docs for new structure Also use sphinx_rtd_theme --- docs/_templates/autosummary/class.rst | 1 - docs/conf.py | 15 ++++++----- docs/reference.rst | 38 +++++++++++++++++++-------- environment.yml | 8 +++--- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst index 09a4d099d..76399d42a 100644 --- a/docs/_templates/autosummary/class.rst +++ b/docs/_templates/autosummary/class.rst @@ -3,7 +3,6 @@ .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} - :show-inheritance: :members: {% block methods %} diff --git a/docs/conf.py b/docs/conf.py index 02447f8a3..b231f5697 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,17 +15,17 @@ # import sys # sys.path.insert(0, os.path.abspath("..")) - from pathlib import Path +import manimpango + # -- Project information ----------------------------------------------------- -from pkg_resources import get_distribution project = "ManimPango" -copyright = "2021, The Manim Community Dev Team" -author = "The Manim Community Dev Team" +copyright = "2021, Naveen M K" +author = "Naveen M K" -release = get_distribution("ManimPango").version +release = manimpango.__version__ version = ".".join(release.split(".")[:2]) @@ -42,6 +42,7 @@ "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinxext.opengraph", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. @@ -58,7 +59,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "furo" +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -76,7 +77,7 @@ add_module_names = False intersphinx_mapping = { - "manim": ("https://docs.manim.community/en/v0.2.0", None), + "manim": ("https://docs.manim.community/en/stable", None), "python": ("https://docs.python.org/3", None), } diff --git a/docs/reference.rst b/docs/reference.rst index 00af64639..68cd617b7 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1,22 +1,38 @@ Manimpango Reference ==================== +.. currentmodule:: manimpango + +************ +Font Manager +************ + .. autosummary:: :toctree: reference - manimpango.TextSetting - manimpango.PangoUtils - manimpango.text2svg - manimpango.MarkupUtils - manimpango.register_font - manimpango.unregister_font - manimpango.list_fonts + ~FontProperties + ~RegisterFont +***** Enums -===== +***** + +.. autosummary:: + :toctree: reference + + ~Style + ~Weight + ~Variant + ~Alignment + +***** +Utils +***** + .. autosummary:: :toctree: reference - manimpango.Style - manimpango.Weight - manimpango.Variant + ~Color + ~pango_version + ~cairo_version + ~list_fonts diff --git a/environment.yml b/environment.yml index 887568285..19ad2aea9 100644 --- a/environment.yml +++ b/environment.yml @@ -1,9 +1,9 @@ -name: manimpango +name: '48' channels: - defaults dependencies: - _libgcc_mutex=0.1=main - - ca-certificates=2020.12.8=h06a4308_0 + - ca-certificates=2021.4.13=h06a4308_1 - cairo=1.14.12=h8948797_3 - certifi=2020.12.5=py37h06a4308_0 - fontconfig=2.13.0=h9420a91_0 @@ -23,7 +23,7 @@ dependencies: - libxcb=1.14=h7b6447c_0 - libxml2=2.9.10=hb55368b_3 - ncurses=6.2=he6710b0_1 - - openssl=1.1.1i=h27cfd23_0 + - openssl=1.1.1k=h27cfd23_0 - pango=1.45.3=hd140c19_0 - pcre=8.44=he6710b0_0 - pip=20.3.3=py37h06a4308_0 @@ -32,6 +32,7 @@ dependencies: - python=3.7.9=h7579374_0 - readline=8.0=h7b6447c_0 - setuptools=51.0.0=py37h06a4308_2 + - sphinx_rtd_theme=0.4.3=py_0 - sqlite=3.33.0=h62c20be_0 - tk=8.6.10=hbc83047_0 - wheel=0.36.2=pyhd3eb1b0_0 @@ -44,7 +45,6 @@ dependencies: - chardet==4.0.0 - cython==0.29.21 - docutils==0.16 - - furo==2021.3.20b30 - idna==2.10 - imagesize==1.2.0 - jinja2==2.11.2 From 2d89912e4737aac90d6821d1c1ceca3ae5aabea8 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sat, 29 May 2021 23:36:04 +0530 Subject: [PATCH 4/4] Fix tests Remove dependency on Manim for tests Disable tests which are not supported --- tests/__init__.py | 4 + tests/_manim.py | 219 --------------------------------- tests/test_font_description.py | 20 +++ tests/test_font_manager.py | 88 +++++++++++++ tests/test_fonts.py | 56 +++++---- tests/test_list_fonts.py | 5 +- tests/test_markup.py | 188 +++++++++++++--------------- tests/test_utils.py | 3 +- 8 files changed, 232 insertions(+), 351 deletions(-) delete mode 100644 tests/_manim.py create mode 100644 tests/test_font_description.py create mode 100644 tests/test_font_manager.py diff --git a/tests/__init__.py b/tests/__init__.py index e09142a4a..45c17aec7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -31,3 +31,7 @@ def delete_media_dir(): delete_media_dir() CASES_DIR = Path(Path(__file__).parent, "cases").absolute() FONT_DIR = Path(__file__).parent / "fonts" + +# avoid fail due to env vars. +os.environ["PANGOCAIRO_BACKEND"] = "" +os.environ["FONTCONFIG_PATH"] = "" diff --git a/tests/_manim.py b/tests/_manim.py deleted file mode 100644 index b11d3e903..000000000 --- a/tests/_manim.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -"""This file contains helpers for the tests copied and modified -from Manim. -""" -import copy -import os -import re -from pathlib import Path - -from manimpango import Alignment, MarkupUtils, TextSetting, text2svg - - -class MarkupText: - def __init__( - self, - text: str, - *, - size: int = 1, - line_spacing: int = None, - font: str = None, - slant: str = "NORMAL", - weight: str = "NORMAL", - tab_width: int = 4, - disable_ligatures: bool = False, - justify: bool = None, - indent: float = None, - alignment: Alignment = None, - # for the tests - filename: str = "test.svg", - wrap_text: bool = True, - **kwargs, - ): - self.text = text - self.size = size - self.line_spacing = line_spacing - self.font = font - self.slant = slant - self.weight = weight - self.tab_width = tab_width - self.filename = filename - self.original_text = text - self.disable_ligatures = disable_ligatures - - self.justify = justify - self.indent = indent - self.alignment = alignment - self.wrap_text = wrap_text - if MarkupUtils.validate(self.text): - raise ValueError( - f"Pango cannot parse your markup in {self.text}. " - "Please check for typos, unmatched tags or unescaped " - "special chars like < and &." - ) - self.text2svg() - - def text2svg(self): - """Convert the text to SVG using Pango.""" - size = self.size * 10 - dir_name = Path(self.filename).parent - disable_liga = self.disable_ligatures - if not os.path.exists(dir_name): - os.makedirs(dir_name) - file_name = self.filename - if self.wrap_text: - return MarkupUtils.text2svg( - f"{self.text}", - self.font, - self.slant, - self.weight, - size, - True, # stray positional argument - disable_liga, - file_name, - 20, - 20, - 600, # width - 400, # height - justify=self.justify, - indent=self.indent, - line_spacing=self.line_spacing, - alignment=self.alignment, - ) - else: - return MarkupUtils.text2svg( - f"{self.text}", - self.font, - self.slant, - self.weight, - size, - True, # stray positional argument - disable_liga, - file_name, - 20, - 20, - 600, # width - 400, # height - justify=self.justify, - indent=self.indent, - line_spacing=self.line_spacing, - alignment=self.alignment, - pango_width=-1, - ) - # -1 for no wrapping - # default is full width and then wrap. - - def __repr__(self): - return f"MarkupText({repr(self.original_text)})" - - -class Text: - def __init__( - self, - text: str, - fill_opacity: float = 1.0, - stroke_width: int = 0, - size: int = 1, - line_spacing: int = -1, - font: str = "", - slant: str = "NORMAL", - weight: str = "NORMAL", - gradient: tuple = None, - tab_width: int = 4, - disable_ligatures: bool = False, - filename: str = "text.svg", - **kwargs, - ) -> None: - self.size = size - self.filename = filename - self.line_spacing = line_spacing - self.font = font - self.slant = slant - self.weight = weight - self.gradient = gradient - self.tab_width = tab_width - self.original_text = text - self.disable_ligatures = disable_ligatures - text_without_tabs = text - self.t2f = self.t2s = self.t2w = {} - if text.find("\t") != -1: - text_without_tabs = text.replace("\t", " " * self.tab_width) - self.text = text_without_tabs - if self.line_spacing == -1: - self.line_spacing = self.size + self.size * 0.3 - else: - self.line_spacing = self.size + self.size * self.line_spacing - self.text2svg() - - def text2settings(self): - """Internally used function. Converts the texts and styles - to a setting for parsing.""" - settings = [] - t2x = [self.t2f, self.t2s, self.t2w] - for i in range(len(t2x)): - fsw = [self.font, self.slant, self.weight] - if t2x[i]: - for word, x in list(t2x[i].items()): - for start, end in self.find_indexes(word, self.text): - fsw[i] = x - settings.append(TextSetting(start, end, *fsw)) - # Set all text settings (default font, slant, weight) - fsw = [self.font, self.slant, self.weight] - settings.sort(key=lambda setting: setting.start) - temp_settings = settings.copy() - start = 0 - for setting in settings: - if setting.start != start: - temp_settings.append(TextSetting(start, setting.start, *fsw)) - start = setting.end - if start != len(self.text): - temp_settings.append(TextSetting(start, len(self.text), *fsw)) - settings = sorted(temp_settings, key=lambda setting: setting.start) - - if re.search(r"\n", self.text): - line_num = 0 - for start, end in self.find_indexes("\n", self.text): - for setting in settings: - if setting.line_num == -1: - setting.line_num = line_num - if start < setting.end: - line_num += 1 - new_setting = copy.copy(setting) - setting.end = end - new_setting.start = end - new_setting.line_num = line_num - settings.append(new_setting) - settings.sort(key=lambda setting: setting.start) - break - for setting in settings: - if setting.line_num == -1: - setting.line_num = 0 - return settings - - def text2svg(self): - """Internally used function. - Convert the text to SVG using Pango - """ - size = self.size * 10 - line_spacing = self.line_spacing * 10 - dir_name = Path(self.filename).parent - disable_liga = self.disable_ligatures - if not os.path.exists(dir_name): - os.makedirs(dir_name) - file_name = self.filename - settings = self.text2settings() - width = 600 - height = 400 - - return text2svg( - settings, - size, - line_spacing, - disable_liga, - file_name, - 30, - 30, - width, - height, - self.text, - ) diff --git a/tests/test_font_description.py b/tests/test_font_description.py new file mode 100644 index 000000000..a6dae97b6 --- /dev/null +++ b/tests/test_font_description.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from manimpango import FontProperties + + +def test_init(): + FontProperties() + + +def test_family_property(): + desc = FontProperties() + assert desc.family is None + desc.family = "Roboto" + assert desc.family == "Roboto" + + +def test_size_property(): + desc = FontProperties() + assert desc.size is None + desc.size = 20 + assert desc.size == 20 diff --git a/tests/test_font_manager.py b/tests/test_font_manager.py new file mode 100644 index 000000000..e23c9fcb8 --- /dev/null +++ b/tests/test_font_manager.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +import sys +from pathlib import Path + +import pytest +from attr.exceptions import FrozenInstanceError + +from manimpango import FontProperties, RegisterFont, Style, Variant, Weight, list_fonts + +from .test_fonts import font_lists + + +def test_invalid_size(): + with pytest.raises(ValueError): + FontProperties(size=0) + + +def test_font_properties_attributes(): + fp = FontProperties( + family="Hello", + size=10, + style=Style.ITALIC, + variant=Variant.NORMAL, + weight=Weight.BOLD, + ) + assert fp.family == "Hello" + assert fp.size == 10 + assert fp.style == Style.ITALIC + assert fp.variant == Variant.NORMAL + assert fp.weight == Weight.BOLD + + +def test_Register_Font_wrapper_frozen(): + a = RegisterFont(list(font_lists.keys())[0]) + with pytest.raises(FrozenInstanceError): + a.family = "" + a.unregister() + + +def test_Register_Font(): + a = RegisterFont(list(font_lists.keys())[1]) + fonts = list_fonts() + assert a.family[0] in fonts + assert isinstance(a.family, list) + a.unregister() + # below one fails due to caching in Pango. + # Maybe we can disable it? + # assert a.family[0] not in fonts + + +@pytest.mark.skipif(sys.platform.startswith("linux"), reason="uses fc by default.") +def test_fc_in_Register_Font(): + a = RegisterFont(list(font_lists.keys())[1], use_fontconfig=True) + fonts = list_fonts() + assert a.family is not None + assert list(font_lists.values())[1] not in fonts + assert a.family[0] not in fonts + a.unregister() + + +def test_fc_in_Register_Font_with_rendering(setup_fontconfig): + a = RegisterFont(list(font_lists.keys())[1], use_fontconfig=True) + fonts = list_fonts() + assert a.family is not None + assert a.family[0] in fonts + a.unregister() + + +def test_Register_Font_without_calculating_family(): + a = RegisterFont(list(font_lists.keys())[1], calculate_family=False) + assert a.family is None + a.unregister() + + +@pytest.mark.parametrize("fontconfig", [True, False]) +def test_Register_Font_invalid_font_raise(tmpdir, fontconfig): + tmpfile = Path(tmpdir) / "nice.ttf" + with tmpfile.open("w") as f: + f.write("test font") + with pytest.raises(RuntimeError): + RegisterFont(tmpfile, use_fontconfig=fontconfig) + + +def test_Register_Font_file_not_found(tmpdir): + with pytest.raises(FileNotFoundError): + RegisterFont(Path(tmpdir) / "test") + with pytest.raises(FileNotFoundError): + RegisterFont(Path(tmpdir)) diff --git a/tests/test_fonts.py b/tests/test_fonts.py index da4289522..3196828be 100644 --- a/tests/test_fonts.py +++ b/tests/test_fonts.py @@ -6,9 +6,14 @@ import pytest import manimpango +from manimpango.font_manager._register_font import ( + fc_register_font, + fc_unregister_font, + register_font, + unregister_font, +) from . import FONT_DIR -from ._manim import MarkupText, Text font_lists = { (FONT_DIR / "AdobeVFPrototype.ttf").absolute(): "Adobe Variable Font Prototype", @@ -22,24 +27,25 @@ def test_unicode_font_name(tmpdir): final_font = str(Path(tmpdir, "庞门正.ttf").absolute()) copyfile(FONT_DIR / "AdobeVFPrototype.ttf", final_font) - assert manimpango.register_font(final_font) - assert manimpango.unregister_font(final_font) + assert register_font(final_font) + assert unregister_font(final_font) @pytest.mark.parametrize("font_name", font_lists) def test_register_font(font_name): intial = manimpango.list_fonts() - assert manimpango.register_font(str(font_name)), "Invalid Font possibly." + assert register_font(str(font_name)), "Invalid Font possibly." final = manimpango.list_fonts() assert intial != final -@pytest.mark.parametrize("font_name", font_lists.values()) -def test_warning(capfd, font_name): - print(font_name) - Text("Testing", font=font_name) - captured = capfd.readouterr() - assert "Pango-WARNING **" not in captured.err, "Looks like pango raised a warning?" +# @pytest.mark.parametrize("font_name", font_lists.values()) +# def test_warning(capfd, font_name): +# print(font_name) +# manim.Text("Testing", font=font_name) +# captured = capfd.readouterr() +# assert "Pango-WARNING **" not in captured.err, +# "Looks like pango raised a warning?" @pytest.mark.skipif( @@ -48,7 +54,7 @@ def test_warning(capfd, font_name): @pytest.mark.parametrize("font_name", font_lists) def test_unregister_font(font_name): intial = manimpango.list_fonts() - assert manimpango.unregister_font(str(font_name)), "Failed to unregister the font" + assert unregister_font(str(font_name)), "Failed to unregister the font" final = manimpango.list_fonts() assert intial != final @@ -58,8 +64,8 @@ def test_unregister_font(font_name): ) @pytest.mark.parametrize("font_name", font_lists) def test_register_and_unregister_font(font_name): - assert manimpango.register_font(str(font_name)), "Invalid Font possibly." - assert manimpango.unregister_font(str(font_name)), "Failed to unregister the font" + assert register_font(str(font_name)), "Invalid Font possibly." + assert unregister_font(str(font_name)), "Failed to unregister the font" @pytest.mark.skipif( @@ -68,9 +74,7 @@ def test_register_and_unregister_font(font_name): @pytest.mark.parametrize("font_name", font_lists) @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="always returns true") def test_fail_just_unregister(font_name): - assert not manimpango.unregister_font( - str(font_name) - ), "Failed to unregister the font" + assert not unregister_font(str(font_name)), "Failed to unregister the font" @pytest.mark.skipif( @@ -78,7 +82,7 @@ def test_fail_just_unregister(font_name): ) @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="unsupported api for mac") def test_unregister_linux(): - assert manimpango.unregister_font("random") + assert unregister_font("random") @pytest.mark.skipif( @@ -88,27 +92,27 @@ def test_adding_dummy_font(tmpdir): dummy = tmpdir / "font.ttf" with open(dummy, "wb") as f: f.write(b"dummy") - assert not manimpango.register_font(str(dummy)), "Registered a dummy font?" + assert not register_font(str(dummy)), "Registered a dummy font?" -def test_simple_fonts_render(tmpdir): - filename = str(Path(tmpdir) / "hello.svg") - MarkupText("Hello World", filename=filename) - assert Path(filename).exists() +# def test_simple_fonts_render(tmpdir): +# filename = str(Path(tmpdir) / "hello.svg") +# MarkupText("Hello World", filename=filename) +# assert Path(filename).exists() @pytest.mark.skipif( not sys.platform.startswith("linux"), reason="unsupported api other than linux" ) def test_both_fc_and_register_font_are_same(): - assert manimpango.fc_register_font == manimpango.register_font - assert manimpango.fc_unregister_font == manimpango.unregister_font + assert fc_register_font == register_font + assert fc_unregister_font == unregister_font @pytest.mark.parametrize("font_file", font_lists) def test_fc_font_register(setup_fontconfig, font_file): intial = manimpango.list_fonts() - assert manimpango.fc_register_font(str(font_file)), "Invalid Font possibly." + assert fc_register_font(str(font_file)), "Invalid Font possibly." final = manimpango.list_fonts() assert intial != final @@ -116,6 +120,6 @@ def test_fc_font_register(setup_fontconfig, font_file): def test_fc_font_unregister(setup_fontconfig): # it will remove everything intial = manimpango.list_fonts() - manimpango.fc_unregister_font("clear") + fc_unregister_font("clear") final = manimpango.list_fonts() assert intial != final diff --git a/tests/test_list_fonts.py b/tests/test_list_fonts.py index 370fb42d8..22e832f35 100644 --- a/tests/test_list_fonts.py +++ b/tests/test_list_fonts.py @@ -4,6 +4,7 @@ import pytest import manimpango +from manimpango.font_manager._register_font import register_font, unregister_font from .test_fonts import font_lists @@ -20,7 +21,7 @@ def test_whether_list(): ) @pytest.mark.parametrize("font_file", font_lists) def test_resgister_font_with_list(font_file): - manimpango.register_font(str(font_file)) + register_font(str(font_file)) a = manimpango.list_fonts() assert font_lists[font_file] in a - manimpango.unregister_font(str(font_file)) + unregister_font(str(font_file)) diff --git a/tests/test_markup.py b/tests/test_markup.py index abb82bbcf..6e3ed5c5b 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -1,122 +1,106 @@ # -*- coding: utf-8 -*- -from pathlib import Path + import pytest import manimpango -from . import CASES_DIR -from ._manim import MarkupText -from .svg_tester import SVGStyleTester +# from .svg_tester import SVGStyleTester -ipsum_text = ( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit," - "sed do eiusmod tempor incididunt ut labore et dolore" - "magna aliqua. Ut enim ad minim veniam, quis nostrud" - "exercitation ullamco laboris nisi ut aliquip" - "ex ea commodo consequat. Duis aute irure dolor" - "in reprehenderit in voluptate velit esse cillum" - "dolore eu fugiat nulla pariatur. Excepteur sint" - "occaecat cupidatat non proident, sunt in culpa qui" - "officia deserunt mollit anim id est laborum." -) +# ipsum_text = ( +# "Lorem ipsum dolor sit amet, consectetur adipiscing elit," +# "sed do eiusmod tempor incididunt ut labore et dolore" +# "magna aliqua. Ut enim ad minim veniam, quis nostrud" +# "exercitation ullamco laboris nisi ut aliquip" +# "ex ea commodo consequat. Duis aute irure dolor" +# "in reprehenderit in voluptate velit esse cillum" +# "dolore eu fugiat nulla pariatur. Excepteur sint" +# "occaecat cupidatat non proident, sunt in culpa qui" +# "officia deserunt mollit anim id est laborum." +# ) @pytest.mark.parametrize("text", ["foo", "bar", "வணக்கம்"]) def test_good_markup(text): - assert not manimpango.MarkupUtils.validate( - text, + assert ( + manimpango.layout.utils.validate_markup( + text, + ) + == "" ), f"{text} should not fail validation" @pytest.mark.parametrize("text", ["foo", "foo"]) def test_bad_markup(text): - assert manimpango.MarkupUtils.validate( - text + assert ( + manimpango.layout.utils.validate_markup(text) != "" ), f"{text} should fail validation (unbalanced tags)" -@pytest.mark.parametrize( - "text,error", - [ - ( - "foo", - "Error on line 1 char 23: Element “markup” was closed, " - "but the currently open element is “b”", - ), - ( - "foo", - "Unknown tag 'xyz' on line 1 char 14", - ), - ], -) -def test_bad_markup_error_message(text, error): - assert manimpango.MarkupUtils.validate(text) == error - - -def test_markup_text(tmpdir): - loc = Path(tmpdir, "test.svg") - assert not loc.exists() - MarkupText( - 'Hello Manim', filename=str(loc) - ) - assert loc.exists() - - -def test_markup_justify(tmpdir): - # don't know how to verify this correctly - # it varies upon diffent system so, we are - # just check whether it runs - loc = Path(tmpdir, "test.svg") - assert not loc.exists() - MarkupText(ipsum_text, justify=True, filename=str(loc)) - assert loc.exists() - - -def test_markup_indent(tmpdir): - # don't know how to verify this correctly - # it varies upon diffent system so, we are - # just check whether it runs - loc = Path(tmpdir, "test.svg") - assert not loc.exists() - MarkupText(ipsum_text, indent=10, filename=str(loc)) - assert loc.exists() - - -def test_markup_alignment(tmpdir): - # don't know how to verify this correctly - # it varies upon diffent system so, we are - # just check whether it runs - loc = Path(tmpdir, "test.svg") - assert not loc.exists() - MarkupText( - ipsum_text, - alignment=manimpango.Alignment.CENTER, - filename=str(loc), - ) - assert loc.exists() - - -def test_markup_style(tmpdir): - test_case = CASES_DIR / "hello_blue_world_green.svg" - expected = tmpdir / "expected.svg" - text = "Hello\nWorld" - MarkupText( - text, - filename=str(expected), - ) - s = SVGStyleTester(gotSVG=expected, expectedSVG=test_case) - assert len(s.got_svg_style) == len(s.expected_svg_style) - assert s.got_svg_style == s.expected_svg_style - - -def test_wrap_text(tmpdir): - tmpdir = Path(tmpdir) - wrapped = tmpdir / "wrap.svg" - nowrap = tmpdir / "nowarap.svg" - - MarkupText(ipsum_text, wrap_text=False, filename=str(nowrap)) - MarkupText(ipsum_text, filename=str(wrapped)) - - assert wrapped.read_text() != nowrap.read_text() +# def test_markup_text(tmpdir): +# loc = Path(tmpdir, "test.svg") +# assert not loc.exists() +# MarkupText( +# 'Hello Manim', filename=str(loc) +# ) +# assert loc.exists() + + +# def test_markup_justify(tmpdir): +# # don't know how to verify this correctly +# # it varies upon diffent system so, we are +# # just check whether it runs +# loc = Path(tmpdir, "test.svg") +# assert not loc.exists() +# MarkupText(ipsum_text, justify=True, filename=str(loc)) +# assert loc.exists() + + +# def test_markup_indent(tmpdir): +# # don't know how to verify this correctly +# # it varies upon diffent system so, we are +# # just check whether it runs +# loc = Path(tmpdir, "test.svg") +# assert not loc.exists() +# MarkupText(ipsum_text, indent=10, filename=str(loc)) +# assert loc.exists() + + +# def test_markup_alignment(tmpdir): +# # don't know how to verify this correctly +# # it varies upon diffent system so, we are +# # just check whether it runs +# loc = Path(tmpdir, "test.svg") +# assert not loc.exists() +# MarkupText( +# ipsum_text, +# alignment=manimpango.Alignment.CENTER, +# filename=str(loc), +# ) +# assert loc.exists() + + +# def test_markup_style(tmpdir): +# test_case = CASES_DIR / "hello_blue_world_green.svg" +# expected = tmpdir / "expected.svg" +# text = ("Hello" +# "\nWorld") +# MarkupText( +# text, +# filename=str(expected), +# ) +# s = SVGStyleTester(gotSVG=expected, expectedSVG=test_case) +# assert len(s.got_svg_style) == len(s.expected_svg_style) +# assert s.got_svg_style == s.expected_svg_style + + +# def test_wrap_text(tmpdir): +# tmpdir = Path(tmpdir) +# wrapped = tmpdir / "wrap.svg" +# nowrap = tmpdir / "nowarap.svg" + +# MarkupText(ipsum_text, wrap_text=False, filename=str(nowrap)) +# MarkupText(ipsum_text, filename=str(wrapped)) + +# assert wrapped.read_text() != nowrap.read_text() diff --git a/tests/test_utils.py b/tests/test_utils.py index 39189f89c..a2bcf0535 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import pytest -from manimpango.enums import Style, Weight -from manimpango.utils import PangoUtils +from manimpango.utils import PangoUtils, Style, Weight def test_str2style():