├── test-requirements.txt ├── pyproject.toml ├── svg ├── defines.h ├── __init__.pyx ├── svg.pyi ├── parser.pyi ├── rasterizer.pyi └── __init__.pxd ├── pytest.ini ├── .gitmodules ├── tests ├── Londonhackspacelogo.png ├── testsvg.py ├── testparse.py ├── testrast.py └── Londonhackspacelogo.svg ├── MANIFEST.in ├── .appveyor.yml ├── svg-stubs └── svg.pyi ├── .travis.yml ├── setup.py ├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.md └── .gitignore /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==5.3 2 | Pillow==7.0 3 | Cython==0.29 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["Cython>=0.27", "setuptools==41.0"] -------------------------------------------------------------------------------- /svg/defines.h: -------------------------------------------------------------------------------- 1 | #define NANOSVG_IMPLEMENTATION 2 | #define NANOSVGRAST_IMPLEMENTATION -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | minversion = 2.8 3 | testpaths = tests/ 4 | python_files = test*.py 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "nanosvg"] 2 | path = nanosvg 3 | url = https://github.com/memononen/nanosvg 4 | -------------------------------------------------------------------------------- /svg/__init__.pyx: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.1" 2 | 3 | include "svg.pyi" 4 | include "parser.pyi" 5 | include "rasterizer.pyi" -------------------------------------------------------------------------------- /tests/Londonhackspacelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmatyping/pynanosvg/HEAD/tests/Londonhackspacelogo.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include svg *.pyx *.pyi *.pxd *.h 2 | recursive-include svg-stubs *.pyi 3 | recursive-include nanosvg * -------------------------------------------------------------------------------- /tests/testsvg.py: -------------------------------------------------------------------------------- 1 | from svg import SVG, Parser 2 | 3 | def test_svg_size(): 4 | svg = Parser.parse_file('tests/Londonhackspacelogo.svg') 5 | assert svg.width == 114 6 | assert svg.height == 114 7 | assert isinstance(svg.width, int) 8 | assert isinstance(svg.height, int) 9 | -------------------------------------------------------------------------------- /tests/testparse.py: -------------------------------------------------------------------------------- 1 | from svg import Parser, SVG 2 | 3 | 4 | def test_parse_file(): 5 | svg = Parser.parse_file('tests/Londonhackspacelogo.svg') 6 | assert isinstance(svg, SVG) 7 | 8 | 9 | def test_parse_str(): 10 | with open('tests/Londonhackspacelogo.svg') as s: 11 | svg = Parser.parse(s.read()) 12 | assert isinstance(svg, SVG) 13 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | - '%LOCALAPPDATA%\pip\Cache' 3 | environment: 4 | CIBW_SKIP: cp27-* cp33-* cp34-* 5 | CIBW_BEFORE_BUILD: pip install -r test-requirements.txt 6 | matrix: 7 | - PYTHON: "C:\\Python35-x64" 8 | PYTHON_VERSION: "3.5.x" 9 | PYTHON_ARCH: "64" 10 | build_script: 11 | - "git submodule update --init" 12 | - "%PYTHON%\\python.exe -m pip install -r test-requirements.txt" 13 | - "%PYTHON%\\python.exe -m pip install cibuildwheel" 14 | - "%PYTHON%\\python.exe -m cibuildwheel --output-dir wheelhouse" 15 | 16 | artifacts: 17 | - path: "wheelhouse\\*.whl" 18 | name: Wheels -------------------------------------------------------------------------------- /svg-stubs/svg.pyi: -------------------------------------------------------------------------------- 1 | 2 | class SVG: 3 | @property 4 | def width(self) -> float: ... 5 | 6 | @property 7 | def height(self) -> float: ... 8 | 9 | class Parser: 10 | @staticmethod 11 | def parse(svg: str, dpi: str = ...) -> SVG: ... 12 | 13 | @staticmethod 14 | def parse_file(filename: str, dpi: str = ...) -> SVG: ... 15 | 16 | class Rasterizer: 17 | def rasterize(self, svg: SVG, width: int, height: int, scale: float = ..., tx: int = ..., ty: int = ...) -> bytes: ... 18 | def rasterize_to_buffer(self, svg: SVG, width: int, height: int, stride: int, buffer: bytes, scale: float = ..., tx: int = ..., ty: int = ...) -> bytes: ... -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - sudo: required 6 | services: 7 | - docker 8 | env: 9 | - PIP=pip 10 | - CIBW_SKIP="cp27-* cp33-* cp34-*" 11 | - CIBW_BEFORE_BUILD="python -m pip install -U pip && pip install ." 12 | - os: osx 13 | language: generic 14 | env: 15 | - PIP=pip 16 | - CIBWSKIP="cp27-* cp33-* cp34-*" 17 | - CIBW_BEFORE_BUILD="python -m pip install -U pip && pip install ." 18 | 19 | script: 20 | - $PIP install -r test-requirements.txt 21 | - $PIP install cibuildwheel==0.10 22 | - python -m pip install -U pip 23 | - $PIP install . 24 | - cibuildwheel --output-dir wheelhouse 25 | 26 | notifications: 27 | email: 28 | on_success: never 29 | -------------------------------------------------------------------------------- /svg/svg.pyi: -------------------------------------------------------------------------------- 1 | cdef class SVG: 2 | """Cython for parsing and getting information about an SVG""" 3 | cdef NSVGimage* _nsvgimage 4 | 5 | def __cinit__(self): 6 | self._nsvgimage = NULL 7 | 8 | def __dealloc__(self): 9 | if self._nsvgimage != NULL: 10 | nsvgDelete(self._nsvgimage) 11 | 12 | @property 13 | def width(self) -> int: 14 | """Returns the width of the svg image.""" 15 | if self._nsvgimage == NULL: 16 | raise ValueError("SVG has not been parsed yet.") 17 | return int(self._nsvgimage.width) 18 | 19 | @property 20 | def height(self) -> int: 21 | """Returns the height of the svg image.""" 22 | if self._nsvgimage == NULL: 23 | raise ValueError("SVG has not been parsed yet.") 24 | return int(self._nsvgimage.height) -------------------------------------------------------------------------------- /tests/testrast.py: -------------------------------------------------------------------------------- 1 | from svg import Parser, Rasterizer 2 | from PIL import Image 3 | 4 | 5 | def test_rast_to_bytes(): 6 | svg = Parser.parse_file('tests/Londonhackspacelogo.svg') 7 | r = Rasterizer() 8 | buff = r.rasterize(svg, svg.width, svg.height) 9 | assert isinstance(buff, bytes) 10 | img = Image.open('tests/Londonhackspacelogo.png') 11 | assert buff == img.tobytes() 12 | 13 | def test_rast_with_buffer(): 14 | svg = Parser.parse_file('tests/Londonhackspacelogo.svg') 15 | r = Rasterizer() 16 | stride = svg.width * 4 17 | buff = bytes(stride * svg.height) 18 | assert isinstance(buff, bytes) 19 | r.rasterize_to_buffer(svg, svg.width, svg.height, stride, buff) 20 | assert isinstance(buff, bytes) 21 | img = Image.open('tests/Londonhackspacelogo.png') 22 | assert buff == img.tobytes() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from Cython.Build import cythonize 3 | import os 4 | from os import path 5 | import sys 6 | 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | with open(path.join(this_directory, 'README.md')) as f: 9 | long_description = f.read() 10 | 11 | ext = [Extension('svg', sources=['svg/__init__.pyx'], 12 | include_dirs=['./nanosvg/src/', './pynanosvg/'])] 13 | 14 | setup( 15 | name="pynanosvg", 16 | version="0.3.1", 17 | description="Wrapper around nanosvg", 18 | author="Ethan Smith", 19 | author_email="ethan@ethanhs.me", 20 | url="https://github.com/ethanhs/pynanosvg", 21 | license="MIT", 22 | ext_modules=cythonize(ext), 23 | packages=['svg-stubs'], 24 | package_data={'svg-stubs': ['svg.pyi']}, 25 | long_description=long_description, 26 | long_description_content_type='text/markdown', 27 | ) -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | tags: ['*'] 8 | pull_request: 9 | paths-ignore: 10 | - '**/*.rst' 11 | - '**/*.md' 12 | - .gitignore 13 | - .travis.yml 14 | - LICENSE 15 | jobs: 16 | tests: 17 | 18 | runs-on: windows-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python: ['3.5', '3.6', '3.7'] 23 | arch: ['x86', 'x64'] 24 | steps: 25 | - uses: actions/checkout@v1 26 | - run: git submodule update --init 27 | - name: Setup Python 28 | uses: actions/setup-python@v1 29 | with: 30 | python-version: ${{ matrix.python }} 31 | architecture: ${{ matrix.arch }} 32 | - name: install requirements 33 | run: python -m pip install -r test-requirements.txt 34 | - run: python -m pip install -U setuptools 35 | - run: python setup.py build_ext 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Ethan Smith 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pynanosvg 2 | 3 | NOTE: nanosvg is no longer actively maintained 4 | 5 | [![Travis](https://img.shields.io/travis/ethanhs/pynanosvg.svg?style=flat-square)](https://travis-ci.org/ethanhs/pynanosvg) | [![AppVeyor](https://img.shields.io/appveyor/ci/ethanhs/pynanosvg-9m0yu.svg?style=flat-square)](https://ci.appveyor.com/project/ethanhs/pynanosvg-9m0yu) 6 | 7 | Pynanosvg is a wrapper around [nanosvg](https://github.com/memononen/nanosvg) 8 | a simple svg parsing library. I created pynanosvg because the only other 9 | options in Python were the librsvg bindings, which are very large! 10 | 11 | # Install 12 | 13 | Simple: 14 | ``` 15 | git clone --recursive https://github.com/ethanhs/pynanosvg.git 16 | cd pynanosvg 17 | python3 -m pip install . 18 | ``` 19 | 20 | or just 21 | 22 | > python3 -m pip install pynanosvg 23 | 24 | # Usage 25 | 26 | The following parses an SVG file, rasterizes it, and saves it as a PNG 27 | 28 | ```python 29 | # import things 30 | from svg import Parser, Rasterizer, SVG 31 | from PIL import Image # for saving rasterized image 32 | # Parse from a file 33 | svg = Parser.parse_file('my_cool_img.svg') 34 | print('Image is {} by {}.'.format(svg.width, svg.height)) 35 | rast = Rasterizer() 36 | buff = rast.rasterize(svg, svg.width, svg.height) 37 | im = Image.frombytes('RGBA', svg.width, svg.height, buff) 38 | im.save('my_cool_img.png') # save the converted image! 39 | ``` 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | #ignore cython generated files 12 | pynanosvg/*.c 13 | 14 | .pytest_cache/ 15 | .vscode/ 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # IPython Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | #pycharm files 100 | .idea/ -------------------------------------------------------------------------------- /svg/parser.pyi: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | class SVGParserError(Exception): 4 | pass 5 | 6 | 7 | VALID_DPI_UNITS = ('px', 'pt', 'pc', 'mm', 'cm', 'in') 8 | 9 | def _dpi_conv(dpi: str): 10 | if dpi[-2:] not in VALID_DPI_UNITS: 11 | raise ValueError("dpi needs to be one of {}, got {}".format(VALID_DPI_UNITS, dpi[-2:])) 12 | units = dpi[-2:].encode('UTF-8') 13 | _dpi = float(dpi[:-2]) 14 | return units, _dpi 15 | 16 | 17 | cdef class Parser: 18 | """ 19 | Cython class for parsing SVG files. 20 | """ 21 | 22 | @staticmethod 23 | def parse(svg, dpi: str = '96px') -> SVG: 24 | """ 25 | Creates an SVG image from an SVG string. Units for dpi are 'px', 'pt', 26 | 'pc' 'mm', 'cm', or 'in'. 27 | """ 28 | units, magnitude = _dpi_conv(dpi) 29 | im = SVG() 30 | if isinstance(svg, str): 31 | s = svg.encode('UTF-8') 32 | elif isinstance(svg, bytes): 33 | s = svg 34 | else: 35 | raise TypeError("svg must be either str or bytes, found {}".format(type(svg))) 36 | im._nsvgimage = nsvgParse(s, units, magnitude) 37 | if im._nsvgimage == NULL: 38 | raise SVGParserError("Could not parse SVG from string.") 39 | else: 40 | return im 41 | 42 | @staticmethod 43 | def parse_file(filename: str, dpi: str = '96px') -> SVG: 44 | """ 45 | Creates an SVG image from filename. Units for dpi are 'px', 'pt', 46 | 'pc' 'mm', 'cm', or 'in'. 47 | """ 48 | file = filename.encode(sys.getfilesystemencoding()) 49 | units, magnitude = _dpi_conv(dpi) 50 | im = SVG() 51 | im._nsvgimage = nsvgParseFromFile(file, units, magnitude) 52 | if im._nsvgimage == NULL: 53 | raise SVGParserError( 54 | "Could not parse SVG from {}".format(file) 55 | ) 56 | else: 57 | return im 58 | -------------------------------------------------------------------------------- /svg/rasterizer.pyi: -------------------------------------------------------------------------------- 1 | import cython 2 | 3 | class SVGRasterizerError(Exception): 4 | pass 5 | 6 | 7 | cdef class Rasterizer: 8 | """Cython class parsing and rendering SVG images.""" 9 | cdef NSVGrasterizer* _nsvgrasterizer 10 | 11 | def __cinit__(self): 12 | self._nsvgrasterizer = nsvgCreateRasterizer() 13 | 14 | def __dealloc__(self): 15 | if self._nsvgrasterizer != NULL: 16 | nsvgDeleteRasterizer(self._nsvgrasterizer) 17 | 18 | def rasterize(self, svg: SVG, 19 | width: cython.int, 20 | height: cython.int, 21 | scale: cython.float = 1.0, 22 | tx: cython.int = 0, 23 | ty: cython.int = 0) -> bytes: 24 | """ 25 | Rasterizes the SVG into a new buffer of bytes forming an RGBA image. 26 | """ 27 | if svg._nsvgimage == NULL: 28 | raise ValueError('The given SVG is empty, you must parse the SVG first.') 29 | # used to calculate size of buffer 30 | length = width * height * 4 31 | stride = width * 4 32 | buff = bytes(length) 33 | nsvgRasterize(self._nsvgrasterizer, 34 | svg._nsvgimage, tx, ty, scale, 35 | buff, width, height, stride) 36 | return buff 37 | 38 | def rasterize_to_buffer(self, svg: SVG, 39 | width: cython.int, 40 | height: cython.int, 41 | stride: cython.int, 42 | buffer: bytes, 43 | scale: cython.float = 1.0, 44 | tx: cython.int = 0, 45 | ty: cython.int = 0, 46 | ): 47 | """ 48 | Rasterizes the SVG into a given buffer, which should be of length width * height * 4. Stride is usually width * 4. 49 | """ 50 | if not isinstance(buffer, bytes): 51 | raise TypeError("`buffer` must be bytes, found {}".format(type(buffer))) 52 | if stride <= 0: 53 | raise ValueError('You must set a stride to rasterize to a buffer, stride must be positive.') 54 | if svg._nsvgimage == NULL: 55 | raise ValueError('The given SVG is empty, you must parse the SVG first.') 56 | nsvgRasterize(self._nsvgrasterizer, 57 | svg._nsvgimage, tx, ty, scale, 58 | buffer, width, height, stride) 59 | return buffer 60 | -------------------------------------------------------------------------------- /svg/__init__.pxd: -------------------------------------------------------------------------------- 1 | # this is a work around to get NANOSVG_IMPLEMENTATION to be defined 2 | cdef extern from 'defines.h': 3 | enum: 4 | NANOSVG_IMPLEMENTATION 5 | NANOSVGRAST_IMPLEMENTATION 6 | 7 | cdef extern from 'nanosvg.h': 8 | 9 | enum NSVGpaintType: 10 | NSVG_PAINT_NONE 11 | NSVG_PAINT_COLOR 12 | NSVG_PAINT_LINEAR_GRADIENT 13 | NSVG_PAINT_RADIAL_GRADIENT 14 | 15 | enum NSVGspreadType: 16 | NSVG_SPREAD_PAD 17 | NSVG_SPREAD_REFLECT 18 | NSVG_SPREAD_REPEAT 19 | 20 | enum NSVGlineJoin: 21 | NSVG_JOIN_MITER 22 | NSVG_JOIN_ROUND 23 | NSVG_JOIN_BEVEL 24 | 25 | enum NSVGlineCap: 26 | NSVG_CAP_BUTT 27 | NSVG_CAP_ROUND 28 | NSVG_CAP_SQUARE 29 | 30 | enum NSVGfillRule: 31 | NSVG_FILLRULE_NONZERO 32 | NSVG_FILLRULE_EVENODD 33 | 34 | enum NSVGflags: 35 | NSVG_FLAGS_VISIBLE 36 | 37 | struct NSVGgradientStop: 38 | unsigned int color 39 | float offset 40 | 41 | struct NSVGgradient: 42 | float xform[6] 43 | char spread 44 | float fx 45 | float fy 46 | int nstops 47 | NSVGgradientStop stops[1] 48 | 49 | struct NSVGpaint: 50 | char type 51 | 52 | struct NSVGpath: 53 | float *pts 54 | int npts 55 | char closed 56 | float bounds[4] 57 | NSVGpath *next 58 | 59 | struct NSVGshape: 60 | char id[64] 61 | NSVGpaint fill 62 | NSVGpaint stroke 63 | float opacity 64 | float strokeWidth 65 | float strokeDashOffset 66 | float strokeDashArray[8] 67 | char strokeDashCount 68 | char strokeLineJoin 69 | char strokeLineCap 70 | char fillRule 71 | unsigned char flags 72 | float bounds[4] 73 | NSVGpath *paths 74 | NSVGshape *next 75 | 76 | struct NSVGimage: 77 | float width 78 | float height 79 | NSVGshape *shapes 80 | 81 | cdef NSVGimage *nsvgParseFromFile(const char *filename, const char *units, float dpi) 82 | 83 | cdef NSVGimage *nsvgParse(char *input, const char *units, float dpi) 84 | 85 | cdef void nsvgDelete(NSVGimage *image) 86 | 87 | 88 | cdef extern from 'nanosvgrast.h': 89 | struct NSVGedge: 90 | pass 91 | 92 | struct NSVGpoint: 93 | pass 94 | 95 | struct NSVGactiveEdge: 96 | pass 97 | 98 | struct NSVGmemPage: 99 | pass 100 | 101 | struct NSVGrasterizer: 102 | float px, py 103 | float tessTol 104 | float distTol 105 | 106 | NSVGedge* edges 107 | int nedges 108 | int cedges 109 | 110 | NSVGpoint* points 111 | int npoints 112 | int cpoints 113 | 114 | NSVGpoint* points2 115 | int npoints2 116 | int cpoints2 117 | 118 | NSVGactiveEdge* freelist 119 | NSVGmemPage* pages 120 | NSVGmemPage* curpage 121 | 122 | unsigned char* scanline 123 | int cscanline 124 | 125 | unsigned char* bitmap 126 | int width, height, stride 127 | 128 | 129 | cdef NSVGrasterizer* nsvgCreateRasterizer() 130 | 131 | cdef void nsvgRasterize(NSVGrasterizer* r, 132 | NSVGimage* image, float tx, float ty, float scale, 133 | unsigned char* dst, int w, int h, int stride) 134 | 135 | cdef void nsvgDeleteRasterizer(NSVGrasterizer*) -------------------------------------------------------------------------------- /tests/Londonhackspacelogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | image/svg+xml 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | --------------------------------------------------------------------------------