├── tests ├── __init__.py ├── data │ ├── logo.png │ ├── logo-bw.png │ ├── logo-actual.png │ ├── logo-actual-bw.png │ ├── logo-expected.png │ ├── logo-expected-bw.png │ ├── logo-actual-skimage.png │ ├── logo-actual-skimagebw.png │ ├── logo-expected-skimage.png │ ├── logo-expected-skimagebw.png │ ├── logo-bw.svg │ ├── logo-asyncBw.svg │ ├── logo.svg │ ├── logo-async.svg │ └── logo-skimage.svg └── test_main.py ├── .gitattributes ├── readme-assets └── icons │ ├── name.png │ └── proj-icon.png ├── requirements.txt ├── documentation ├── reference │ ├── README.md │ └── svgtrace │ │ └── index.md └── tutorials │ └── README.md ├── svgtrace ├── imagetracer.html ├── __init__.py └── imagetracer.js ├── .github └── workflows │ ├── mirror-to-codeberg.yaml │ └── test-lint.yaml ├── LICENSE.md ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── pyproject.toml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | svgtrace/imagetracer.js linguist-vendored 2 | -------------------------------------------------------------------------------- /tests/data/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo.png -------------------------------------------------------------------------------- /tests/data/logo-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-bw.png -------------------------------------------------------------------------------- /tests/data/logo-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-actual.png -------------------------------------------------------------------------------- /readme-assets/icons/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/readme-assets/icons/name.png -------------------------------------------------------------------------------- /tests/data/logo-actual-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-actual-bw.png -------------------------------------------------------------------------------- /tests/data/logo-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-expected.png -------------------------------------------------------------------------------- /tests/data/logo-expected-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-expected-bw.png -------------------------------------------------------------------------------- /readme-assets/icons/proj-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/readme-assets/icons/proj-icon.png -------------------------------------------------------------------------------- /tests/data/logo-actual-skimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-actual-skimage.png -------------------------------------------------------------------------------- /tests/data/logo-actual-skimagebw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-actual-skimagebw.png -------------------------------------------------------------------------------- /tests/data/logo-expected-skimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-expected-skimage.png -------------------------------------------------------------------------------- /tests/data/logo-expected-skimagebw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FHPythonUtils/SvgTrace/HEAD/tests/data/logo-expected-skimagebw.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | install-playwright<2,>=0.0.1 2 | numpy<2,>=1.26.4 3 | pillow<11,>=10.2.0 4 | playwright<2,>=1.42.0 5 | scikit-image<2,>=0.22.0 6 | -------------------------------------------------------------------------------- /documentation/reference/README.md: -------------------------------------------------------------------------------- 1 | # Svgtrace Index 2 | 3 | > Auto-generated documentation index. 4 | 5 | A full list of `Svgtrace` project modules. 6 | 7 | - [Svgtrace](svgtrace/index.md#svgtrace) 8 | -------------------------------------------------------------------------------- /svgtrace/imagetracer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ImageTrace 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/mirror-to-codeberg.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Sync repo to the Codeberg mirror 3 | name: Repo sync GitHub -> Codeberg 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | 9 | jobs: 10 | codeberg: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: spyoungtech/mirror-action@v0.7.0 17 | with: 18 | REMOTE: "https://codeberg.org/FredHappyface/SvgTrace.git" 19 | GIT_USERNAME: FredHappyface 20 | GIT_PASSWORD: ${{ secrets.CODEBERG_PASSWORD }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test-lint.yaml: -------------------------------------------------------------------------------- 1 | name: Python Test and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Python Test and Lint 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version: 19 | - '3.9' 20 | - '3.10' 21 | - '3.11' 22 | - '3.12' 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Install Poetry 34 | run: | 35 | curl -sSL https://install.python-poetry.org | python3 - 36 | 37 | - name: Install dependencies 38 | run: poetry install 39 | 40 | - name: Run pytest 41 | run: poetry run pytest 42 | 43 | - name: Run ruff 44 | run: poetry run ruff check --output-format=github 45 | continue-on-error: true 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Kieran BW 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.3.3 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | 9 | - repo: https://github.com/RobertCraigie/pyright-python 10 | rev: v1.1.354 11 | hooks: 12 | - id: pyright 13 | 14 | - repo: https://github.com/Lucas-C/pre-commit-hooks-safety 15 | rev: v1.3.3 16 | hooks: 17 | - id: python-safety-dependencies-check 18 | files: pyproject.toml 19 | 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: v4.5.0 22 | hooks: 23 | - id: trailing-whitespace 24 | - id: end-of-file-fixer 25 | - id: check-case-conflict 26 | - id: check-executables-have-shebangs 27 | - id: check-json 28 | - id: check-merge-conflict 29 | - id: check-shebang-scripts-are-executable 30 | - id: check-symlinks 31 | - id: check-toml 32 | - id: check-vcs-permalinks 33 | - id: check-yaml 34 | - id: detect-private-key 35 | - id: mixed-line-ending 36 | 37 | - repo: https://github.com/boidolr/pre-commit-images 38 | rev: v1.5.2 39 | hooks: 40 | - id: optimize-jpg 41 | - id: optimize-png 42 | - id: optimize-svg 43 | - id: optimize-webp 44 | 45 | exclude: "tests/data|documentation/reference" 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All major and minor version changes will be documented in this file. Details of 4 | patch-level version changes can be found in [commit messages](../../commits/master). 5 | 6 | ## 2024 - 2024/01/07 7 | 8 | - update dependencies 9 | 10 | ## 2023.1 - 2023/06/25 11 | 12 | - Add `asyncTrace` and `skimageTrace` 13 | - Raise OSError "svgtrace.trace/ asyncTrace is not supported in Windows Jupyter Notebooks" 14 | - Documentation: Add Tutorials 15 | 16 | ## 2023.0.1 - 2023/03/12 17 | 18 | - Raise `FileNotFoundError` when input does not exist 19 | 20 | ## 2023 - 2023/01/08 21 | 22 | - Add tox config 23 | - Use playwright and install_playwright in-place of pyppeteer 24 | 25 | ## 2022 - 2022/01/06 26 | 27 | - update precommit 28 | - update documentation 29 | - improve tests (run with pytest - generates output) 30 | - update deps 31 | - remove `metprint` 32 | 33 | ## 2021.0.2 - 2021/11/14 34 | 35 | - add pre-commit 36 | - update readme 37 | - update deps 38 | 39 | ## 2021.0.1 - 2021/05/01 40 | 41 | - Reformatting 42 | - Improve test code a bit 43 | 44 | ## 2021 - 2021/01/06 45 | 46 | - Fixed bug resulting in failed trace 47 | 48 | ## 2020.1 - 2020/10/29 49 | 50 | - Added typing 51 | - Drop py < 3.7 52 | 53 | ## 2020.0.3 - 2020/07/12 54 | 55 | - Updated pyppeteer to 0.2.2 56 | 57 | ## 2020.0.2 - 2020/05/06 58 | 59 | - Updated classifiers 60 | - Added docs 61 | 62 | ## 2020.0.1 - 2020/04/25 63 | 64 | - Corrected unvendor imagetracer.js to vendor it (rookie error 😁) 65 | - Updated project URLs to point to SvgTrace and not MetPrint 66 | 67 | ## 2020 - 2020/04/24 68 | 69 | - Use the imageedit svg tracer in a standalone package. 70 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | """Test the image tracer.""" 2 | 3 | import asyncio 4 | import sys 5 | from pathlib import Path 6 | 7 | import imgcompare 8 | import pytest 9 | from nocairosvg import svg2bitmap 10 | from PIL import Image 11 | 12 | THISDIR = str(Path(__file__).resolve().parent) 13 | sys.path.insert(0, str(Path(THISDIR).parent)) 14 | logoFile = f"{THISDIR}/data/logo" 15 | notExistsFile = f"{THISDIR}/data/notExists" 16 | 17 | from svgtrace import asyncTrace, skimageTrace, trace 18 | 19 | 20 | def aux_comparesvg(svgpath: str, pngpath: str) -> bool: 21 | output = f"{THISDIR}/data/{pngpath}" 22 | svg2bitmap(url=svgpath, write_to=output) 23 | imgcompare.is_equal(output.replace("-actual", "-expected"), output, tolerance=0.2) 24 | 25 | 26 | def test_bw() -> None: 27 | Path(f"{logoFile}-bw.svg").write_text(trace(f"{logoFile}-bw.png", True), encoding="utf-8") 28 | aux_comparesvg(f"{logoFile}-bw.svg", "logo-actual-bw.png") 29 | 30 | 31 | def test_colour() -> None: 32 | Path(f"{logoFile}.svg").write_text(trace(f"{logoFile}.png"), encoding="utf-8") 33 | aux_comparesvg(f"{logoFile}.svg", "logo-actual.png") 34 | 35 | 36 | def test_asyncBw() -> None: 37 | Path(f"{logoFile}-asyncBw.svg").write_text( 38 | asyncio.run(asyncTrace(f"{logoFile}-bw.png", True)), encoding="utf-8" 39 | ) 40 | aux_comparesvg(f"{logoFile}-asyncBw.svg", "logo-actual-bw.png") 41 | 42 | 43 | def test_asyncColour() -> None: 44 | Path(f"{logoFile}-async.svg").write_text( 45 | asyncio.run(asyncTrace(f"{logoFile}.png")), encoding="utf-8" 46 | ) 47 | aux_comparesvg(f"{logoFile}-async.svg", "logo-actual.png") 48 | 49 | 50 | def test_skimageBw() -> None: 51 | Path(f"{logoFile}-skimagebw.svg").write_text( 52 | skimageTrace(Image.open(f"{logoFile}-bw.png")), encoding="utf-8" 53 | ) 54 | aux_comparesvg(f"{logoFile}-skimagebw.svg", "logo-actual-skimagebw.png") 55 | 56 | 57 | def test_skimageColour() -> None: 58 | Path(f"{logoFile}-skimage.svg").write_text( 59 | skimageTrace(Image.open(f"{logoFile}.png")), encoding="utf-8" 60 | ) 61 | aux_comparesvg(f"{logoFile}-skimage.svg", "logo-actual-skimage.png") 62 | 63 | 64 | def test_notExists() -> None: 65 | with pytest.raises(FileNotFoundError): 66 | Path(f"{notExistsFile}.svg").write_text(trace(f"{notExistsFile}.png"), encoding="utf-8") 67 | -------------------------------------------------------------------------------- /documentation/tutorials/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tutorial 3 | 4 | See below for a step-by-step tutorial on how to use svgtrace 5 | 6 | - [Using the `trace` function](#using-the-trace-function) 7 | - [Using the `asyncTrace` function](#using-the-asynctrace-function) 8 | - [Using the `skimageTrace` function](#using-the-skimagetrace-function) 9 | 10 | ## Using the `trace` function 11 | 12 | 1. Create the following files (`logo-bw.png` and `logo.png`) 13 |
14 | Screenshot 1 15 | Screenshot 2 16 |
17 | 18 | 2. Import `svgtrace` and convert a file on disk (with `trace`) to an svg, andsave this back to disk 19 | (with `Path().write_text`) 20 | ```python 21 | from pathlib import Path 22 | from svgtrace import trace 23 | 24 | Path("logo-bw.svg").write_text(trace("logo-bw.png", True), encoding="utf-8") 25 | Path("logo.svg").write_text(trace("logo.png"), encoding="utf-8") 26 | ``` 27 | 28 | 3. Output 29 |
30 | logo-wb.svg 31 | logo.svg 32 |
33 | 34 | ## Using the `asyncTrace` function 35 | 36 | 1. Import `svgtrace` and convert a file on disk (with `asyncTrace`) to an svg, andsave this back to disk 37 | (with `Path().write_text`) 38 | ```python 39 | from pathlib import Path 40 | from svgtrace import trace 41 | 42 | Path("logo-asyncBw.svg").write_text(asyncio.run(asyncTrace("logo-bw.png", True)), encoding="utf-8") 43 | Path("logo-async.svg").write_text(asyncio.run(asyncTrace("logo.png")), encoding="utf-8") 44 | ``` 45 | 46 | 2. Output is identical to above 47 | 48 | ## Using the `skimageTrace` function 49 | 50 | 1. Import `svgtrace` and convert a pillow image (with `skimageTrace`) to an svg, andsave this back to disk 51 | (with `Path().write_text`) 52 | ```python 53 | from pathlib import Path 54 | from PIL import Image 55 | from svgtrace import trace 56 | 57 | Path("logo-skimageBw.svg").write_text(skimageTrace(Image.open("logo-bw.png")), encoding="utf-8") 58 | Path("logo-skimage.svg").write_text(skimageTrace(Image.open("logo.png")), encoding="utf-8") 59 | ``` 60 | 61 | 2. Output 62 |
63 | logo-skimageBw.svg 64 | logo-skimage.svg 65 |
66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "svgtrace" 3 | version = "2024" 4 | license = "mit" 5 | description = "Leverage playwright and the imagetrace.js library to trace a bitmap to svg in python" 6 | authors = ["FredHappyface"] 7 | classifiers = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "Intended Audience :: Developers", 10 | "Intended Audience :: Education", 11 | "Natural Language :: English", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python :: Implementation :: CPython", 14 | "Topic :: Multimedia :: Graphics", 15 | "Topic :: Software Development :: Libraries :: Python Modules", 16 | "Topic :: Utilities" 17 | ] 18 | homepage = "https://github.com/FHPythonUtils/SvgTrace" 19 | repository = "https://github.com/FHPythonUtils/SvgTrace" 20 | documentation = "https://github.com/FHPythonUtils/SvgTrace/blob/master/README.md" 21 | readme = "README.md" 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.9" 25 | playwright = "<2,>=1.42.0" 26 | install-playwright = "<2,>=0.0.1" 27 | scikit-image = "<2,>=0.22.0" 28 | pillow = "<11,>=10.2.0" 29 | numpy = "<2,>=1.26.4" 30 | 31 | [tool.poetry.group.dev.dependencies] 32 | nocairosvg = "^2024" 33 | pytest = "^8.1.1" 34 | handsdown = "^2.1.0" 35 | coverage = "^7.4.4" 36 | ruff = "^0.3.3" 37 | pyright = "^1.1.354" 38 | imgcompare = "^2.0.1" 39 | 40 | [build-system] 41 | requires = ["poetry-core"] 42 | build-backend = "poetry.core.masonry.api" 43 | 44 | [tool.ruff] 45 | line-length = 100 46 | indent-width = 4 47 | target-version = "py38" 48 | 49 | [tool.ruff.lint] 50 | select = ["ALL"] 51 | ignore = [ 52 | "ANN101", # type annotation for self in method 53 | "COM812", # enforce trailing comma 54 | "D2", # pydocstyle formatting 55 | "ISC001", 56 | "N", # pep8 naming 57 | "PLR09", # pylint refactor too many 58 | "TCH", # type check blocks 59 | "W191" # ignore this to allow tabs 60 | ] 61 | fixable = ["ALL"] 62 | 63 | [tool.ruff.lint.per-file-ignores] 64 | "**/{tests,docs,tools}/*" = ["D", "S101", "E402"] 65 | 66 | [tool.ruff.lint.flake8-tidy-imports] 67 | ban-relative-imports = "all" # Disallow all relative imports. 68 | 69 | [tool.ruff.format] 70 | indent-style = "tab" 71 | docstring-code-format = true 72 | line-ending = "lf" 73 | 74 | [tool.pyright] 75 | venvPath = "." 76 | venv = ".venv" 77 | 78 | [tool.coverage.run] 79 | branch = true 80 | 81 | [tool.tox] 82 | legacy_tox_ini = """ 83 | [tox] 84 | env_list = 85 | py311 86 | py310 87 | py39 88 | 89 | [testenv] 90 | deps = 91 | nocairosvg 92 | pytest 93 | imgcompare 94 | commands = pytest tests 95 | """ 96 | -------------------------------------------------------------------------------- /documentation/reference/svgtrace/index.md: -------------------------------------------------------------------------------- 1 | # Svgtrace 2 | 3 | [Svgtrace Index](../README.md#svgtrace-index) / Svgtrace 4 | 5 | > Auto-generated documentation for [svgtrace](../../../svgtrace/__init__.py) module. 6 | 7 | - [Svgtrace](#svgtrace) 8 | - [asyncTrace](#asynctrace) 9 | - [skimageTrace](#skimagetrace) 10 | - [trace](#trace) 11 | 12 | ## asyncTrace 13 | 14 | [Show source in __init__.py:46](../../../svgtrace/__init__.py#L46) 15 | 16 | Do a trace of an image on the filesystem using the playwright library. 17 | 18 | #### Arguments 19 | 20 | ---- 21 | - `filename` *str* - The location of the file on the filesystem, use an 22 | absolute filepath for this 23 | - `blackAndWhite` *bool, optional* - Trace a black and white SVG. Defaults to False. 24 | - `mode` *str, optional* - Set the mode. See https://github.com/jankovicsandras/imagetracerjs 25 | for more information. Defaults to "default". 26 | 27 | #### Returns 28 | 29 | ------- 30 | - `str` - SVG string 31 | 32 | #### Raises 33 | 34 | ------ 35 | FileNotFoundError f"{filename} does not exist!" 36 | OSError "svgtrace.trace/ asyncTrace is not supported in Windows Jupyter Notebooks" 37 | 38 | #### Signature 39 | 40 | ```python 41 | async def asyncTrace( 42 | filename: str, blackAndWhite: bool = False, mode: str = "default" 43 | ) -> str: ... 44 | ``` 45 | 46 | 47 | 48 | ## skimageTrace 49 | 50 | [Show source in __init__.py:113](../../../svgtrace/__init__.py#L113) 51 | 52 | Do a trace of an pillow image using the skimage library. 53 | 54 | #### Arguments 55 | 56 | ---- 57 | - `image` *Image.Image* - pillow image to trace 58 | 59 | #### Returns 60 | 61 | ------- 62 | - `str` - SVG string 63 | 64 | #### Signature 65 | 66 | ```python 67 | def skimageTrace(image: Image.Image) -> str: ... 68 | ``` 69 | 70 | 71 | 72 | ## trace 73 | 74 | [Show source in __init__.py:23](../../../svgtrace/__init__.py#L23) 75 | 76 | Do a trace of an image on the filesystem using the playwright library. 77 | 78 | #### Arguments 79 | 80 | ---- 81 | - `filename` *str* - The location of the file on the filesystem, use an 82 | absolute filepath for this 83 | - `blackAndWhite` *bool, optional* - Trace a black and white SVG. Defaults to False. 84 | - `mode` *str, optional* - Set the mode. See https://github.com/jankovicsandras/imagetracerjs 85 | for more information. Defaults to "default". 86 | 87 | #### Returns 88 | 89 | ------- 90 | - `str` - SVG string 91 | 92 | #### Raises 93 | 94 | ------ 95 | FileNotFoundError f"{filename} does not exist!" 96 | 97 | #### Signature 98 | 99 | ```python 100 | def trace(filename: str, blackAndWhite: bool = False, mode: str = "default") -> str: ... 101 | ``` -------------------------------------------------------------------------------- /tests/data/logo-bw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/logo-asyncBw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.rst 2 | requirements_optional.txt 3 | 4 | # DepHell stuff 5 | poetry.lock 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | -------------------------------------------------------------------------------- /tests/data/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/logo-async.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svgtrace/__init__.py: -------------------------------------------------------------------------------- 1 | """Author FredHappyface 2020. 2 | 3 | Uses playwright to leverage a headless version of Chromium 4 | Requires imagetracer.html and imagetracer.js along with the modules below 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import asyncio 10 | import sys 11 | import warnings 12 | from pathlib import Path 13 | 14 | import numpy as np 15 | from install_playwright import install 16 | from PIL import Image 17 | from playwright.async_api import async_playwright 18 | from skimage import feature, measure 19 | 20 | THISDIR = str(Path(__file__).resolve().parent) 21 | 22 | 23 | def trace(filename: str, blackAndWhite: bool = False, mode: str = "default") -> str: 24 | """Do a trace of an image on the filesystem using the playwright library. 25 | 26 | Args: 27 | ---- 28 | filename (str): The location of the file on the filesystem, use an 29 | absolute filepath for this 30 | blackAndWhite (bool, optional): Trace a black and white SVG. Defaults to False. 31 | mode (str, optional): Set the mode. See https://github.com/jankovicsandras/imagetracerjs 32 | for more information. Defaults to "default". 33 | 34 | Returns: 35 | ------- 36 | str: SVG string 37 | 38 | Raises: 39 | ------ 40 | FileNotFoundError f"{filename} does not exist!" 41 | 42 | """ 43 | return asyncio.run(asyncTrace(filename, blackAndWhite, mode)) 44 | 45 | 46 | async def asyncTrace(filename: str, blackAndWhite: bool = False, mode: str = "default") -> str: 47 | """Do a trace of an image on the filesystem using the playwright library. 48 | 49 | Args: 50 | ---- 51 | filename (str): The location of the file on the filesystem, use an 52 | absolute filepath for this 53 | blackAndWhite (bool, optional): Trace a black and white SVG. Defaults to False. 54 | mode (str, optional): Set the mode. See https://github.com/jankovicsandras/imagetracerjs 55 | for more information. Defaults to "default". 56 | 57 | Returns: 58 | ------- 59 | str: SVG string 60 | 61 | Raises: 62 | ------ 63 | FileNotFoundError f"{filename} does not exist!" 64 | OSError "svgtrace.trace/ asyncTrace is not supported in Windows Jupyter Notebooks" 65 | 66 | """ 67 | # Detecting Default event loop in Notebook 6.1.6 on Windows is not ProactorEventLoop 68 | # (https://github.com/jupyter/notebook/issues/5916) 69 | if sys.platform.startswith("win") and sys.version_info >= (3, 8): 70 | try: 71 | from asyncio import WindowsSelectorEventLoopPolicy 72 | except ImportError: 73 | pass 74 | else: 75 | if isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): 76 | msg = "svgtrace.trace/ asyncTrace is not supported in Windows Jupyter Notebooks" 77 | raise OSError( 78 | msg 79 | ) 80 | 81 | # 82 | if mode.find("black") >= 0 or blackAndWhite: 83 | mode = "posterized1" 84 | 85 | filename = filename.replace("\\", "/") 86 | 87 | if not Path(filename).exists(): 88 | msg = f"{filename} does not exist!" 89 | raise FileNotFoundError(msg) 90 | 91 | async with async_playwright() as p: 92 | install(p.chromium) 93 | browser = await p.chromium.launch( 94 | args=[ 95 | "--no-sandbox", 96 | "--disable-web-security", 97 | "--allow-file-access-from-files", 98 | ] 99 | ) 100 | 101 | page = await browser.new_page() 102 | await page.goto(f"file:///{THISDIR}/imagetracer.html") 103 | await page.evaluate( 104 | f"ImageTracer.imageToSVG('file:///{filename}',function(svgstr){{ ImageTracer.appendSVGString( svgstr, 'svg-container' ); }},'{mode}');" 105 | ) 106 | element = await page.query_selector("div") 107 | svg = await page.evaluate("(element) => element.innerHTML", element) 108 | 109 | await browser.close() 110 | return svg 111 | 112 | 113 | def skimageTrace(image: Image.Image) -> str: 114 | """Do a trace of an pillow image using the skimage library. 115 | 116 | Args: 117 | ---- 118 | image (Image.Image): pillow image to trace 119 | 120 | Returns: 121 | ------- 122 | str: SVG string 123 | 124 | """ 125 | with warnings.catch_warnings(): 126 | warnings.filterwarnings("ignore") 127 | imageGreyscale = image.convert("L") 128 | edges = feature.canny(np.array(imageGreyscale)) 129 | paths = [] 130 | contours = measure.find_contours(edges, 0.5) 131 | for contour in contours: 132 | pathData = " L ".join([f"{coord[1]},{coord[0]}" for coord in contour]) 133 | paths.append(f'') 134 | 135 | return ( 136 | f'' 137 | + "".join(paths) 138 | + "" 139 | ) 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub top language](https://img.shields.io/github/languages/top/FHPythonUtils/SvgTrace.svg?style=for-the-badge&cacheSeconds=28800)](../../) 2 | [![Issues](https://img.shields.io/github/issues/FHPythonUtils/SvgTrace.svg?style=for-the-badge&cacheSeconds=28800)](../../issues) 3 | [![License](https://img.shields.io/github/license/FHPythonUtils/SvgTrace.svg?style=for-the-badge&cacheSeconds=28800)](/LICENSE.md) 4 | [![Commit activity](https://img.shields.io/github/commit-activity/m/FHPythonUtils/SvgTrace.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 5 | [![Last commit](https://img.shields.io/github/last-commit/FHPythonUtils/SvgTrace.svg?style=for-the-badge&cacheSeconds=28800)](../../commits/master) 6 | [![PyPI Downloads](https://img.shields.io/pypi/dm/svgtrace.svg?style=for-the-badge&cacheSeconds=28800)](https://pypistats.org/packages/svgtrace) 7 | [![PyPI Total Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&label=total%20downloads&query=%24.total_downloads&url=https%3A%2F%2Fapi%2Epepy%2Etech%2Fapi%2Fv2%2Fprojects%2Fsvgtrace)](https://pepy.tech/project/svgtrace) 8 | [![PyPI Version](https://img.shields.io/pypi/v/svgtrace.svg?style=for-the-badge&cacheSeconds=28800)](https://pypi.org/project/svgtrace) 9 | 10 | 11 | # SvgTrace 12 | 13 | Project Icon 14 | 15 | Leverage playwright and the imagetrace.js library to trace a bitmap to SVG in 16 | python 17 | 18 | - [Documentation](#documentation) 19 | - [Install With PIP](#install-with-pip) 20 | - [Language information](#language-information) 21 | - [Built for](#built-for) 22 | - [Install Python on Windows](#install-python-on-windows) 23 | - [Chocolatey](#chocolatey) 24 | - [Windows - Python.org](#windows---pythonorg) 25 | - [Install Python on Linux](#install-python-on-linux) 26 | - [Apt](#apt) 27 | - [Dnf](#dnf) 28 | - [Install Python on MacOS](#install-python-on-macos) 29 | - [Homebrew](#homebrew) 30 | - [MacOS - Python.org](#macos---pythonorg) 31 | - [How to run](#how-to-run) 32 | - [Windows](#windows) 33 | - [Linux/ MacOS](#linux-macos) 34 | - [Building](#building) 35 | - [Testing](#testing) 36 | - [Download Project](#download-project) 37 | - [Clone](#clone) 38 | - [Using The Command Line](#using-the-command-line) 39 | - [Using GitHub Desktop](#using-github-desktop) 40 | - [Download Zip File](#download-zip-file) 41 | - [Community Files](#community-files) 42 | - [Licence](#licence) 43 | - [Changelog](#changelog) 44 | - [Code of Conduct](#code-of-conduct) 45 | - [Contributing](#contributing) 46 | - [Security](#security) 47 | - [Support](#support) 48 | - [Rationale](#rationale) 49 | 50 | ## Documentation 51 | 52 | A high-level overview of how the documentation is organized organized will help you know 53 | where to look for certain things: 54 | 55 | - [Tutorials](/documentation/tutorials) take you by the hand through a series of steps to get 56 | started using the software. Start here if you’re new. 57 | - The [Technical Reference](/documentation/reference) documents APIs and other aspects of the 58 | machinery. This documentation describes how to use the classes and functions at a lower level 59 | and assume that you have a good high-level understanding of the software. 60 | 64 | 65 | ## Install With PIP 66 | 67 | ```python 68 | pip install svgtrace 69 | ``` 70 | 71 | Head to https://pypi.org/project/svgtrace/ for more info 72 | 73 | ## Language information 74 | 75 | ### Built for 76 | 77 | This program has been written for Python versions 3.8 - 3.11 and has been tested with both 3.8 and 78 | 3.11 79 | 80 | ## Install Python on Windows 81 | 82 | ### Chocolatey 83 | 84 | ```powershell 85 | choco install python 86 | ``` 87 | 88 | ### Windows - Python.org 89 | 90 | To install Python, go to https://www.python.org/downloads/windows/ and download the latest 91 | version. 92 | 93 | ## Install Python on Linux 94 | 95 | ### Apt 96 | 97 | ```bash 98 | sudo apt install python3.x 99 | ``` 100 | 101 | ### Dnf 102 | 103 | ```bash 104 | sudo dnf install python3.x 105 | ``` 106 | 107 | ## Install Python on MacOS 108 | 109 | ### Homebrew 110 | 111 | ```bash 112 | brew install python@3.x 113 | ``` 114 | 115 | ### MacOS - Python.org 116 | 117 | To install Python, go to https://www.python.org/downloads/macos/ and download the latest 118 | version. 119 | 120 | ## How to run 121 | 122 | ### Windows 123 | 124 | - Module 125 | `py -3.x -m [module]` or `[module]` (if module installs a script) 126 | 127 | - File 128 | `py -3.x [file]` or `./[file]` 129 | 130 | ### Linux/ MacOS 131 | 132 | - Module 133 | `python3.x -m [module]` or `[module]` (if module installs a script) 134 | 135 | - File 136 | `python3.x [file]` or `./[file]` 137 | 138 | ## Building 139 | 140 | This project uses https://github.com/FHPythonUtils/FHMake to automate most of the building. This 141 | command generates the documentation, updates the requirements.txt and builds the library artefacts 142 | 143 | Note the functionality provided by fhmake can be approximated by the following 144 | 145 | ```sh 146 | handsdown --cleanup -o documentation/reference 147 | poetry export -f requirements.txt --output requirements.txt 148 | poetry export -f requirements.txt --with dev --output requirements_optional.txt 149 | poetry build 150 | ``` 151 | 152 | `fhmake audit` can be run to perform additional checks 153 | 154 | ## Testing 155 | 156 | For testing with the version of python used by poetry use 157 | 158 | ```sh 159 | poetry run pytest 160 | ``` 161 | 162 | Alternatively use `tox` to run tests over python 3.8 - 3.11 163 | 164 | ```sh 165 | tox 166 | ``` 167 | 168 | ## Download Project 169 | 170 | ### Clone 171 | 172 | #### Using The Command Line 173 | 174 | 1. Press the Clone or download button in the top right 175 | 2. Copy the URL (link) 176 | 3. Open the command line and change directory to where you wish to 177 | clone to 178 | 4. Type 'git clone' followed by URL in step 2 179 | ```bash 180 | git clone https://github.com/FHPythonUtils/SvgTrace 181 | ``` 182 | 183 | More information can be found at 184 | https://help.github.com/en/articles/cloning-a-repository 185 | 186 | #### Using GitHub Desktop 187 | 188 | 1. Press the Clone or download button in the top right 189 | 2. Click open in desktop 190 | 3. Choose the path for where you want and click Clone 191 | 192 | More information can be found at 193 | https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop 194 | 195 | ### Download Zip File 196 | 197 | 1. Download this GitHub repository 198 | 2. Extract the zip archive 199 | 3. Copy/ move to the desired location 200 | 201 | ## Community Files 202 | 203 | ### Licence 204 | 205 | MIT License 206 | Copyright (c) FredHappyface 207 | (See the [LICENSE](/LICENSE.md) for more information.) 208 | 209 | ### Changelog 210 | 211 | See the [Changelog](/CHANGELOG.md) for more information. 212 | 213 | ### Code of Conduct 214 | 215 | Online communities include people from many backgrounds. The *Project* 216 | contributors are committed to providing a friendly, safe and welcoming 217 | environment for all. Please see the 218 | [Code of Conduct](https://github.com/FHPythonUtils/.github/blob/master/CODE_OF_CONDUCT.md) 219 | for more information. 220 | 221 | ### Contributing 222 | 223 | Contributions are welcome, please see the 224 | [Contributing Guidelines](https://github.com/FHPythonUtils/.github/blob/master/CONTRIBUTING.md) 225 | for more information. 226 | 227 | ### Security 228 | 229 | Thank you for improving the security of the project, please see the 230 | [Security Policy](https://github.com/FHPythonUtils/.github/blob/master/SECURITY.md) 231 | for more information. 232 | 233 | ### Support 234 | 235 | Thank you for using this project, I hope it is of use to you. Please be aware that 236 | those involved with the project often do so for fun along with other commitments 237 | (such as work, family, etc). Please see the 238 | [Support Policy](https://github.com/FHPythonUtils/.github/blob/master/SUPPORT.md) 239 | for more information. 240 | 241 | ### Rationale 242 | 243 | The rationale acts as a guide to various processes regarding projects such as 244 | the versioning scheme and the programming styles used. Please see the 245 | [Rationale](https://github.com/FHPythonUtils/.github/blob/master/RATIONALE.md) 246 | for more information. 247 | -------------------------------------------------------------------------------- /tests/data/logo-skimage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svgtrace/imagetracer.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Minor tweaks for bw svg tracing added by fredhappyface. refactoring This file is exempt from 4 | the MIT License 5 | imagetracer.js version 1.2.5 6 | Simple raster image tracer and vectorizer written in JavaScript. 7 | andras@jankovics.net 8 | */ 9 | 10 | /* 11 | 12 | The Unlicense / PUBLIC DOMAIN 13 | 14 | This is free and unencumbered software released into the public domain. 15 | 16 | Anyone is free to copy, modify, publish, use, compile, sell, or 17 | distribute this software, either in source code form or as a compiled 18 | binary, for any purpose, commercial or non-commercial, and by any 19 | means. 20 | 21 | In jurisdictions that recognize copyright laws, the author or authors 22 | of this software dedicate any and all copyright interest in the 23 | software to the public domain. We make this dedication for the benefit 24 | of the public at large and to the detriment of our heirs and 25 | successors. We intend this dedication to be an overt act of 26 | relinquishment in perpetuity of all present and future rights to this 27 | software under copyright law. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 30 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 31 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 32 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 33 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 34 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 35 | OTHER DEALINGS IN THE SOFTWARE. 36 | 37 | For more information, please refer to http://unlicense.org/ 38 | 39 | */ 40 | 41 | (function() { 42 | "use strict"; 43 | 44 | function ImageTracer() { 45 | const _this = this; 46 | 47 | this.versionnumber = "1.2.5", 48 | 49 | // 50 | // User friendly functions 51 | // 52 | 53 | // Loading an image from a URL, tracing when loaded, 54 | // then executing callback with the scaled svg string as argument 55 | this.imageToSVG = function(url, callback, options) { 56 | options = _this.checkoptions(options); 57 | // loading image, tracing and callback 58 | _this.loadImage( 59 | url, 60 | function(canvas) { 61 | callback( 62 | _this.imagedataToSVG(_this.getImgdata(canvas), options), 63 | ); 64 | }, 65 | options, 66 | ); 67 | }, // End of imageToSVG() 68 | 69 | // Tracing imagedata, then returning the scaled svg string 70 | this.imagedataToSVG = function(imgd, options) { 71 | options = _this.checkoptions(options); 72 | // tracing imagedata 73 | const td = _this.imagedataToTracedata(imgd, options); 74 | // returning SVG string 75 | return _this.getsvgstring(td, options); 76 | }, // End of imagedataToSVG() 77 | 78 | // Loading an image from a URL, tracing when loaded, 79 | // then executing callback with tracedata as argument 80 | this.imageToTracedata = function(url, callback, options) { 81 | options = _this.checkoptions(options); 82 | // loading image, tracing and callback 83 | _this.loadImage( 84 | url, 85 | function(canvas) { 86 | callback( 87 | _this.imagedataToTracedata(_this.getImgdata(canvas), options), 88 | ); 89 | }, 90 | options, 91 | ); 92 | }, // End of imageToTracedata() 93 | 94 | // Tracing imagedata, then returning tracedata (layers with paths, 95 | // palette, image size) 96 | this.imagedataToTracedata = function(imgd, options) { 97 | options = _this.checkoptions(options); 98 | 99 | // 1. Color quantization 100 | const ii = _this.colorquantization(imgd, options); 101 | 102 | if (options.layering === 0) {// Sequential layering 103 | // create tracedata object 104 | var tracedata = { 105 | layers: [], 106 | palette: ii.palette, 107 | width: ii.array[0].length - 2, 108 | height: ii.array.length - 2, 109 | }; 110 | 111 | // Loop to trace each color layer 112 | for (let colornum = 0; colornum < ii.palette.length; colornum++) { 113 | // layeringstep -> pathscan -> internodes -> batchtracepaths 114 | const tracedlayer = 115 | _this.batchtracepaths( 116 | 117 | _this.internodes( 118 | 119 | _this.pathscan( 120 | _this.layeringstep(ii, colornum), 121 | options.pathomit, 122 | ), 123 | 124 | options, 125 | 126 | ), 127 | 128 | options.ltres, 129 | options.qtres, 130 | 131 | ); 132 | 133 | // adding traced layer 134 | tracedata.layers.push(tracedlayer); 135 | }// End of color loop 136 | } else {// Parallel layering 137 | // 2. Layer separation and edge detection 138 | const ls = _this.layering(ii); 139 | 140 | // Optional edge node visualization 141 | if (options.layercontainerid) { 142 | _this.drawLayers(ls, _this.specpalette, options.scale, options.layercontainerid); 143 | } 144 | 145 | // 3. Batch pathscan 146 | const bps = _this.batchpathscan(ls, options.pathomit); 147 | 148 | // 4. Batch interpollation 149 | const bis = _this.batchinternodes(bps, options); 150 | 151 | // 5. Batch tracing and creating tracedata object 152 | var tracedata = { 153 | layers: _this.batchtracelayers(bis, options.ltres, options.qtres), 154 | palette: ii.palette, 155 | width: imgd.width, 156 | height: imgd.height, 157 | }; 158 | } // End of parallel layering 159 | // return tracedata 160 | return tracedata; 161 | }, // End of imagedataToTracedata() 162 | this.optionpresets = { 163 | "default": { 164 | // Tracing 165 | corsenabled: false, 166 | ltres: 1, 167 | qtres: 1, 168 | pathomit: 8, 169 | rightangleenhance: true, 170 | 171 | // Color quantization 172 | colorsampling: 2, 173 | numberofcolors: 16, 174 | mincolorratio: 0, 175 | colorquantcycles: 3, 176 | 177 | // Layering method 178 | layering: 0, 179 | 180 | // SVG rendering 181 | strokewidth: 1, 182 | linefilter: false, 183 | scale: 1, 184 | roundcoords: 1, 185 | viewbox: false, 186 | desc: false, 187 | lcpr: 0, 188 | qcpr: 0, 189 | 190 | // Blur 191 | blurradius: 0, 192 | blurdelta: 20, 193 | 194 | }, 195 | "posterized1": {colorsampling: 0, numberofcolors: 2, bw: 1}, 196 | "posterized2": {numberofcolors: 4, blurradius: 5}, 197 | "curvy": {ltres: 0.01, linefilter: true, rightangleenhance: false}, 198 | "sharp": {qtres: 0.01, linefilter: false}, 199 | "detailed": {pathomit: 0, roundcoords: 2, ltres: 0.5, qtres: 0.5, numberofcolors: 64}, 200 | "smoothed": {blurradius: 5, blurdelta: 64}, 201 | "grayscale": {colorsampling: 0, colorquantcycles: 1, numberofcolors: 7}, 202 | "fixedpalette": {colorsampling: 0, colorquantcycles: 1, numberofcolors: 27}, 203 | "randomsampling1": {colorsampling: 1, numberofcolors: 8}, 204 | "randomsampling2": {colorsampling: 1, numberofcolors: 64}, 205 | "artistic1": {colorsampling: 0, colorquantcycles: 1, pathomit: 0, blurradius: 5, blurdelta: 64, ltres: 0.01, linefilter: true, numberofcolors: 16, strokewidth: 2}, 206 | "artistic2": {qtres: 0.01, colorsampling: 0, colorquantcycles: 1, numberofcolors: 4, strokewidth: 0}, 207 | "artistic3": {qtres: 10, ltres: 10, numberofcolors: 8}, 208 | "artistic4": {qtres: 10, ltres: 10, numberofcolors: 64, blurradius: 5, blurdelta: 256, strokewidth: 2}, 209 | "posterized3": { 210 | ltres: 1, qtres: 1, pathomit: 20, rightangleenhance: true, colorsampling: 0, numberofcolors: 3, 211 | mincolorratio: 0, colorquantcycles: 3, blurradius: 3, blurdelta: 20, strokewidth: 0, linefilter: false, 212 | roundcoords: 1, pal: [{r: 0, g: 0, b: 100, a: 255}, {r: 255, g: 255, b: 255, a: 255}], 213 | }, 214 | }, // End of optionpresets 215 | 216 | // creating options object, setting defaults for missing values 217 | this.checkoptions = function(options) { 218 | options = options || {}; 219 | // Option preset 220 | if (typeof options === "string") { 221 | options = options.toLowerCase(); 222 | if (_this.optionpresets[options]) { 223 | options = _this.optionpresets[options]; 224 | } else { 225 | options = {}; 226 | } 227 | } 228 | // Defaults 229 | const ok = Object.keys(_this.optionpresets["default"]); 230 | for (let k = 0; k < ok.length; k++) { 231 | if (!options.hasOwnProperty(ok[k])) { 232 | options[ok[k]] = _this.optionpresets["default"][ok[k]]; 233 | } 234 | } 235 | // options.pal is not defined here, the custom palette should be added externally: options.pal = [ { "r":0, "g":0, "b":0, "a":255 }, {...}, ... ]; 236 | // options.layercontainerid is not defined here, can be added externally: options.layercontainerid = "mydiv"; ...
237 | return options; 238 | }, // End of checkoptions() 239 | 240 | // 241 | // Vectorizing functions 242 | // 243 | 244 | // 1. Color quantization 245 | // Using a form of k-means clustering repeatead options.colorquantcycles times. http://en.wikipedia.org/wiki/Color_quantization 246 | this.colorquantization = function(imgd, options) { 247 | const arr = []; 248 | let idx = 0; 249 | let cd; 250 | let cdl; 251 | let ci; 252 | const paletteacc = []; 253 | const pixelnum = imgd.width * imgd.height; 254 | let i; 255 | let j; 256 | let k; 257 | let cnt; 258 | let palette; 259 | 260 | // imgd.data must be RGBA, not just RGB 261 | if (imgd.data.length < pixelnum * 4) { 262 | const newimgddata = new Uint8ClampedArray(pixelnum * 4); 263 | for (let pxcnt = 0; pxcnt < pixelnum; pxcnt++) { 264 | newimgddata[pxcnt * 4] = imgd.data[pxcnt * 3]; 265 | newimgddata[pxcnt * 4 + 1] = imgd.data[pxcnt * 3 + 1]; 266 | newimgddata[pxcnt * 4 + 2] = imgd.data[pxcnt * 3 + 2]; 267 | newimgddata[pxcnt * 4 + 3] = 255; 268 | } 269 | imgd.data = newimgddata; 270 | }// End of RGBA imgd.data check 271 | 272 | // Filling arr (color index array) with -1 273 | for (j = 0; j < imgd.height + 2; j++) { 274 | arr[j] = []; 275 | for (i = 0; i < imgd.width + 2; i++) { 276 | arr[j][i] = -1; 277 | } 278 | } 279 | 280 | // Use custom palette if pal is defined or sample / generate custom length palette 281 | if (options.pal) { 282 | palette = options.pal; 283 | } else if (options.colorsampling === 0) { 284 | palette = _this.generatepalette(options.numberofcolors); 285 | } else if (options.colorsampling === 1) { 286 | palette = _this.samplepalette(options.numberofcolors, imgd); 287 | } else { 288 | palette = _this.samplepalette2(options.numberofcolors, imgd); 289 | } 290 | 291 | // Selective Gaussian blur preprocessing 292 | if (options.blurradius > 0) { 293 | imgd = _this.blur(imgd, options.blurradius, options.blurdelta); 294 | } 295 | 296 | // Repeat clustering step options.colorquantcycles times 297 | for (cnt = 0; cnt < options.colorquantcycles; cnt++) { 298 | // Average colors from the second iteration 299 | if (cnt > 0) { 300 | // averaging paletteacc for palette 301 | for (k = 0; k < palette.length; k++) { 302 | // averaging 303 | if (paletteacc[k].n > 0) { 304 | palette[k] = { 305 | r: Math.floor(paletteacc[k].r / paletteacc[k].n), 306 | g: Math.floor(paletteacc[k].g / paletteacc[k].n), 307 | b: Math.floor(paletteacc[k].b / paletteacc[k].n), 308 | a: Math.floor(paletteacc[k].a / paletteacc[k].n), 309 | }; 310 | } 311 | 312 | // Randomizing a color, if there are too few pixels and there will be a new cycle 313 | if ((paletteacc[k].n / pixelnum < options.mincolorratio) && (cnt < options.colorquantcycles - 1)) { 314 | palette[k] = { 315 | r: Math.floor(Math.random() * 255), 316 | g: Math.floor(Math.random() * 255), 317 | b: Math.floor(Math.random() * 255), 318 | a: Math.floor(Math.random() * 255), 319 | }; 320 | } 321 | }// End of palette loop 322 | }// End of Average colors from the second iteration 323 | 324 | // Reseting palette accumulator for averaging 325 | for (i = 0; i < palette.length; i++) { 326 | paletteacc[i] = { 327 | r: 0, g: 0, b: 0, a: 0, n: 0, 328 | }; 329 | } 330 | 331 | // loop through all pixels 332 | for (j = 0; j < imgd.height; j++) { 333 | for (i = 0; i < imgd.width; i++) { 334 | // pixel index 335 | idx = (j * imgd.width + i) * 4; 336 | 337 | // find closest color from palette by measuring (rectilinear) color distance between this pixel and all palette colors 338 | ci = 0; cdl = 1024; // 4 * 256 is the maximum RGBA distance 339 | for (k = 0; k < palette.length; k++) { 340 | // In my experience, https://en.wikipedia.org/wiki/Rectilinear_distance works better than https://en.wikipedia.org/wiki/Euclidean_distance 341 | cd = Math.abs(palette[k].r - imgd.data[idx]) + Math.abs(palette[k].g - imgd.data[idx + 1]) + Math.abs(palette[k].b - imgd.data[idx + 2]) + Math.abs(palette[k].a - imgd.data[idx + 3]); 342 | 343 | // Remember this color if this is the closest yet 344 | if (cd < cdl) { 345 | cdl = cd; ci = k; 346 | } 347 | }// End of palette loop 348 | 349 | // add to palettacc 350 | paletteacc[ci].r += imgd.data[idx]; 351 | paletteacc[ci].g += imgd.data[idx + 1]; 352 | paletteacc[ci].b += imgd.data[idx + 2]; 353 | paletteacc[ci].a += imgd.data[idx + 3]; 354 | paletteacc[ci].n++; 355 | 356 | // update the indexed color array 357 | arr[j + 1][i + 1] = ci; 358 | }// End of i loop 359 | }// End of j loop 360 | }// End of Repeat clustering step options.colorquantcycles times 361 | 362 | return { 363 | array: arr, palette: palette, 364 | }; 365 | }, // End of colorquantization() 366 | 367 | // Sampling a palette from imagedata 368 | this.samplepalette = function(numberofcolors, imgd) { 369 | var idx; 370 | var palette = []; 371 | for (var i = 0; i < numberofcolors; i++) { 372 | idx = Math.floor(Math.random() * imgd.data.length / 4) * 4; 373 | palette.push({ 374 | r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], 375 | }); 376 | } 377 | return palette; 378 | }, // End of samplepalette() 379 | 380 | // Deterministic sampling a palette from imagedata: rectangular grid 381 | this.samplepalette2 = function(numberofcolors, imgd) { 382 | var idx, palette = [], ni = Math.ceil(Math.sqrt(numberofcolors)), nj = Math.ceil(numberofcolors / ni), 383 | vx = imgd.width / (ni + 1), vy = imgd.height / (nj + 1); 384 | for (var j = 0; j < nj; j++) { 385 | for (var i = 0; i < ni; i++) { 386 | if (palette.length === numberofcolors) { 387 | break; 388 | } else { 389 | idx = Math.floor(((j + 1) * vy) * imgd.width + ((i + 1) * vx)) * 4; 390 | palette.push({ 391 | r: imgd.data[idx], g: imgd.data[idx + 1], b: imgd.data[idx + 2], a: imgd.data[idx + 3], 392 | }); 393 | } 394 | } 395 | } 396 | return palette; 397 | }, // End of samplepalette2() 398 | 399 | // Generating a palette with numberofcolors 400 | this.generatepalette = function(numberofcolors) { 401 | var palette = [], rcnt, gcnt, bcnt; 402 | if (numberofcolors < 8) { 403 | // Grayscale 404 | var graystep = Math.floor(255 / (numberofcolors - 1)); 405 | for (var i = 0; i < numberofcolors; i++) { 406 | palette.push({ 407 | r: i * graystep, g: i * graystep, b: i * graystep, a: 255, 408 | }); 409 | } 410 | } else { 411 | // RGB color cube 412 | var colorqnum = Math.floor(Math.pow(numberofcolors, 1 / 3)), // Number of points on each edge on the RGB color cube 413 | colorstep = Math.floor(255 / (colorqnum - 1)), // distance between points 414 | rndnum = numberofcolors - colorqnum * colorqnum * colorqnum; // number of random colors 415 | 416 | for (rcnt = 0; rcnt < colorqnum; rcnt++) { 417 | for (gcnt = 0; gcnt < colorqnum; gcnt++) { 418 | for (bcnt = 0; bcnt < colorqnum; bcnt++) { 419 | palette.push({ 420 | r: rcnt * colorstep, g: gcnt * colorstep, b: bcnt * colorstep, a: 255, 421 | }); 422 | }// End of blue loop 423 | }// End of green loop 424 | }// End of red loop 425 | 426 | // Rest is random 427 | for (rcnt = 0; rcnt < rndnum; rcnt++) { 428 | palette.push({ 429 | r: Math.floor(Math.random() * 255), g: Math.floor(Math.random() * 255), b: Math.floor(Math.random() * 255), a: Math.floor(Math.random() * 255), 430 | }); 431 | } 432 | }// End of numberofcolors check 433 | return palette; 434 | }, // End of generatepalette() 435 | 436 | // 2. Layer separation and edge detection 437 | // Edge node types ( ▓: this layer or 1; ░: not this layer or 0 ) 438 | // 12 ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ 439 | // 48 ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓ 440 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 441 | this.layering = function(ii) { 442 | // Creating layers for each indexed color in arr 443 | var layers = [], val = 0, ah = ii.array.length, aw = ii.array[0].length, n1, n2, n3, n4, n5, n6, n7, n8, i, j, k; 444 | 445 | // Create layers 446 | for (k = 0; k < ii.palette.length; k++) { 447 | layers[k] = []; 448 | for (j = 0; j < ah; j++) { 449 | layers[k][j] = []; 450 | for (i = 0; i < aw; i++) { 451 | layers[k][j][i] = 0; 452 | } 453 | } 454 | } 455 | 456 | // Looping through all pixels and calculating edge node type 457 | for (j = 1; j < ah - 1; j++) { 458 | for (i = 1; i < aw - 1; i++) { 459 | // This pixel"s indexed color 460 | val = ii.array[j][i]; 461 | 462 | // Are neighbor pixel colors the same? 463 | n1 = ii.array[j - 1][i - 1] === val ? 1 : 0; 464 | n2 = ii.array[j - 1][i] === val ? 1 : 0; 465 | n3 = ii.array[j - 1][i + 1] === val ? 1 : 0; 466 | n4 = ii.array[j][i - 1] === val ? 1 : 0; 467 | n5 = ii.array[j][i + 1] === val ? 1 : 0; 468 | n6 = ii.array[j + 1][i - 1] === val ? 1 : 0; 469 | n7 = ii.array[j + 1][i] === val ? 1 : 0; 470 | n8 = ii.array[j + 1][i + 1] === val ? 1 : 0; 471 | 472 | // this pixel"s type and looking back on previous pixels 473 | layers[val][j + 1][i + 1] = 1 + n5 * 2 + n8 * 4 + n7 * 8; 474 | if (!n4) { 475 | layers[val][j + 1][i] = 0 + 2 + n7 * 4 + n6 * 8; 476 | } 477 | if (!n2) { 478 | layers[val][j][i + 1] = 0 + n3 * 2 + n5 * 4 + 8; 479 | } 480 | if (!n1) { 481 | layers[val][j][i] = 0 + n2 * 2 + 4 + n4 * 8; 482 | } 483 | }// End of i loop 484 | }// End of j loop 485 | return layers; 486 | }, // End of layering() 487 | 488 | // 2. Layer separation and edge detection 489 | // Edge node types ( ▓: this layer or 1; ░: not this layer or 0 ) 490 | // 12 ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ 491 | // 48 ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓ 492 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 493 | this.layeringstep = function(ii, cnum) { 494 | // Creating layers for each indexed color in arr 495 | var layer = [], ah = ii.array.length, aw = ii.array[0].length, i, j; 496 | 497 | // Create layer 498 | for (j = 0; j < ah; j++) { 499 | layer[j] = []; 500 | for (i = 0; i < aw; i++) { 501 | layer[j][i] = 0; 502 | } 503 | } 504 | 505 | // Looping through all pixels and calculating edge node type 506 | for (j = 1; j < ah; j++) { 507 | for (i = 1; i < aw; i++) { 508 | layer[j][i] = 509 | (ii.array[j - 1][i - 1] === cnum ? 1 : 0) + 510 | (ii.array[j - 1][i] === cnum ? 2 : 0) + 511 | (ii.array[j][i - 1] === cnum ? 8 : 0) + 512 | (ii.array[j][i] === cnum ? 4 : 0) 513 | ; 514 | }// End of i loop 515 | }// End of j loop 516 | 517 | return layer; 518 | }, // End of layeringstep() 519 | 520 | // Lookup tables for pathscan 521 | // pathscan_combined_lookup[ arr[py][px] ][ dir ] = [nextarrpypx, nextdir, deltapx, deltapy]; 522 | this.pathscan_combined_lookup = [ 523 | [[-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1]], // arr[py][px]===0 is invalid 524 | [[0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0]], 525 | [[-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0]], 526 | [[0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1]], 527 | 528 | [[-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1]], 529 | [[13, 3, 0, 1], [13, 2, -1, 0], [7, 1, 0, -1], [7, 0, 1, 0]], 530 | [[-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1]], 531 | [[0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1]], 532 | 533 | [[0, 3, 0, 1], [0, 2, -1, 0], [-1, -1, -1, -1], [-1, -1, -1, -1]], 534 | [[-1, -1, -1, -1], [0, 1, 0, -1], [-1, -1, -1, -1], [0, 3, 0, 1]], 535 | [[11, 1, 0, -1], [14, 0, 1, 0], [14, 3, 0, 1], [11, 2, -1, 0]], 536 | [[-1, -1, -1, -1], [0, 0, 1, 0], [0, 3, 0, 1], [-1, -1, -1, -1]], 537 | 538 | [[0, 0, 1, 0], [-1, -1, -1, -1], [0, 2, -1, 0], [-1, -1, -1, -1]], 539 | [[-1, -1, -1, -1], [-1, -1, -1, -1], [0, 1, 0, -1], [0, 0, 1, 0]], 540 | [[0, 1, 0, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [0, 2, -1, 0]], 541 | [[-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1]], // arr[py][px]===15 is invalid 542 | ], 543 | 544 | // 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest. 545 | // Walk directions (dir): 0 > ; 1 ^ ; 2 < ; 3 v 546 | this.pathscan = function(arr, pathomit) { 547 | var paths = [], pacnt = 0, pcnt = 0, px = 0, py = 0, w = arr[0].length, h = arr.length, 548 | dir = 0, pathfinished = true, holepath = false, lookuprow; 549 | 550 | for (var j = 0; j < h; j++) { 551 | for (var i = 0; i < w; i++) { 552 | if ((arr[j][i] === 4) || (arr[j][i] === 11)) { // Other values are not valid 553 | // Init 554 | px = i; py = j; 555 | paths[pacnt] = {}; 556 | paths[pacnt].points = []; 557 | paths[pacnt].boundingbox = [px, py, px, py]; 558 | paths[pacnt].holechildren = []; 559 | pathfinished = false; 560 | pcnt = 0; 561 | holepath = (arr[j][i] === 11); 562 | dir = 1; 563 | 564 | // Path points loop 565 | while (!pathfinished) { 566 | // New path point 567 | paths[pacnt].points[pcnt] = {}; 568 | paths[pacnt].points[pcnt].x = px - 1; 569 | paths[pacnt].points[pcnt].y = py - 1; 570 | paths[pacnt].points[pcnt].t = arr[py][px]; 571 | 572 | // Bounding box 573 | if ((px - 1) < paths[pacnt].boundingbox[0]) { 574 | paths[pacnt].boundingbox[0] = px - 1; 575 | } 576 | if ((px - 1) > paths[pacnt].boundingbox[2]) { 577 | paths[pacnt].boundingbox[2] = px - 1; 578 | } 579 | if ((py - 1) < paths[pacnt].boundingbox[1]) { 580 | paths[pacnt].boundingbox[1] = py - 1; 581 | } 582 | if ((py - 1) > paths[pacnt].boundingbox[3]) { 583 | paths[pacnt].boundingbox[3] = py - 1; 584 | } 585 | 586 | // Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward 587 | lookuprow = _this.pathscan_combined_lookup[arr[py][px]][dir]; 588 | arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3]; 589 | 590 | // Close path 591 | if ((px - 1 === paths[pacnt].points[0].x) && (py - 1 === paths[pacnt].points[0].y)) { 592 | pathfinished = true; 593 | 594 | // Discarding paths shorter than pathomit 595 | if (paths[pacnt].points.length < pathomit) { 596 | paths.pop(); 597 | } else { 598 | paths[pacnt].isholepath = holepath ? true : false; 599 | 600 | // Finding the parent shape for this hole 601 | if (holepath) { 602 | var parentidx = 0, parentbbox = [-1, -1, w + 1, h + 1]; 603 | for (var parentcnt = 0; parentcnt < pacnt; parentcnt++) { 604 | if ((!paths[parentcnt].isholepath) && 605 | _this.boundingboxincludes(paths[parentcnt].boundingbox, paths[pacnt].boundingbox) && 606 | _this.boundingboxincludes(parentbbox, paths[parentcnt].boundingbox) 607 | ) { 608 | parentidx = parentcnt; 609 | parentbbox = paths[parentcnt].boundingbox; 610 | } 611 | } 612 | paths[parentidx].holechildren.push(pacnt); 613 | }// End of holepath parent finding 614 | pacnt++; 615 | } 616 | }// End of Close path 617 | pcnt++; 618 | }// End of Path points loop 619 | }// End of Follow path 620 | }// End of i loop 621 | }// End of j loop 622 | 623 | return paths; 624 | }, // End of pathscan() 625 | 626 | this.boundingboxincludes = function(parentbbox, childbbox) { 627 | return ((parentbbox[0] < childbbox[0]) && (parentbbox[1] < childbbox[1]) && (parentbbox[2] > childbbox[2]) && (parentbbox[3] > childbbox[3])); 628 | }, // End of boundingboxincludes() 629 | 630 | // 3. Batch pathscan 631 | this.batchpathscan = function(layers, pathomit) { 632 | var bpaths = []; 633 | for (var k in layers) { 634 | if (!layers.hasOwnProperty(k)) { 635 | continue; 636 | } 637 | bpaths[k] = _this.pathscan(layers[k], pathomit); 638 | } 639 | return bpaths; 640 | }, 641 | 642 | // 4. interpollating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE ) 643 | this.internodes = function(paths, options) { 644 | var ins = [], palen = 0, nextidx = 0, nextidx2 = 0, previdx = 0, previdx2 = 0, pacnt, pcnt; 645 | 646 | // paths loop 647 | for (pacnt = 0; pacnt < paths.length; pacnt++) { 648 | ins[pacnt] = {}; 649 | ins[pacnt].points = []; 650 | ins[pacnt].boundingbox = paths[pacnt].boundingbox; 651 | ins[pacnt].holechildren = paths[pacnt].holechildren; 652 | ins[pacnt].isholepath = paths[pacnt].isholepath; 653 | palen = paths[pacnt].points.length; 654 | 655 | // pathpoints loop 656 | for (pcnt = 0; pcnt < palen; pcnt++) { 657 | // next and previous point indexes 658 | nextidx = (pcnt + 1) % palen; nextidx2 = (pcnt + 2) % palen; previdx = (pcnt - 1 + palen) % palen; previdx2 = (pcnt - 2 + palen) % palen; 659 | 660 | // right angle enhance 661 | if (options.rightangleenhance && _this.testrightangle(paths[pacnt], previdx2, previdx, pcnt, nextidx, nextidx2)) { 662 | // Fix previous direction 663 | if (ins[pacnt].points.length > 0) { 664 | ins[pacnt].points[ins[pacnt].points.length - 1].linesegment = _this.getdirection( 665 | ins[pacnt].points[ins[pacnt].points.length - 1].x, 666 | ins[pacnt].points[ins[pacnt].points.length - 1].y, 667 | paths[pacnt].points[pcnt].x, 668 | paths[pacnt].points[pcnt].y, 669 | ); 670 | } 671 | 672 | // This corner point 673 | ins[pacnt].points.push({ 674 | x: paths[pacnt].points[pcnt].x, 675 | y: paths[pacnt].points[pcnt].y, 676 | linesegment: _this.getdirection( 677 | paths[pacnt].points[pcnt].x, 678 | paths[pacnt].points[pcnt].y, 679 | ((paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2), 680 | ((paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2), 681 | ), 682 | }); 683 | } // End of right angle enhance 684 | 685 | // interpolate between two path points 686 | ins[pacnt].points.push({ 687 | x: ((paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2), 688 | y: ((paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2), 689 | linesegment: _this.getdirection( 690 | ((paths[pacnt].points[pcnt].x + paths[pacnt].points[nextidx].x) / 2), 691 | ((paths[pacnt].points[pcnt].y + paths[pacnt].points[nextidx].y) / 2), 692 | ((paths[pacnt].points[nextidx].x + paths[pacnt].points[nextidx2].x) / 2), 693 | ((paths[pacnt].points[nextidx].y + paths[pacnt].points[nextidx2].y) / 2), 694 | ), 695 | }); 696 | }// End of pathpoints loop 697 | }// End of paths loop 698 | return ins; 699 | }, // End of internodes() 700 | 701 | this.testrightangle = function(path, idx1, idx2, idx3, idx4, idx5) { 702 | return (((path.points[idx3].x === path.points[idx1].x) && 703 | (path.points[idx3].x === path.points[idx2].x) && 704 | (path.points[idx3].y === path.points[idx4].y) && 705 | (path.points[idx3].y === path.points[idx5].y) 706 | ) || 707 | ((path.points[idx3].y === path.points[idx1].y) && 708 | (path.points[idx3].y === path.points[idx2].y) && 709 | (path.points[idx3].x === path.points[idx4].x) && 710 | (path.points[idx3].x === path.points[idx5].x) 711 | ) 712 | ); 713 | }, // End of testrightangle() 714 | 715 | this.getdirection = function(x1, y1, x2, y2) { 716 | var val = 8; 717 | if (x1 < x2) { 718 | if (y1 < y2) { 719 | val = 1; 720 | }// SouthEast 721 | else if (y1 > y2) { 722 | val = 7; 723 | }// NE 724 | else { 725 | val = 0; 726 | }// E 727 | } else if (x1 > x2) { 728 | if (y1 < y2) { 729 | val = 3; 730 | }// SW 731 | else if (y1 > y2) { 732 | val = 5; 733 | }// NW 734 | else { 735 | val = 4; 736 | }// W 737 | } else { 738 | if (y1 < y2) { 739 | val = 2; 740 | }// S 741 | else if (y1 > y2) { 742 | val = 6; 743 | }// N 744 | else { 745 | val = 8; 746 | }// center, this should not happen 747 | } 748 | return val; 749 | }, // End of getdirection() 750 | 751 | // 4. Batch interpollation 752 | this.batchinternodes = function(bpaths, options) { 753 | var binternodes = []; 754 | for (var k in bpaths) { 755 | if (!bpaths.hasOwnProperty(k)) { 756 | continue; 757 | } 758 | binternodes[k] = _this.internodes(bpaths[k], options); 759 | } 760 | return binternodes; 761 | }, 762 | 763 | // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path 764 | 765 | // 5.1. Find sequences of points with only 2 segment types 766 | // 5.2. Fit a straight line on the sequence 767 | // 5.3. If the straight line fails (distance error > ltres), find the point with the biggest error 768 | // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence 769 | // 5.5. If the spline fails (distance error > qtres), find the point with the biggest error, set splitpoint = fitting point 770 | // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences 771 | 772 | this.tracepath = function(path, ltres, qtres) { 773 | var pcnt = 0, segtype1, segtype2, seqend, smp = {}; 774 | smp.segments = []; 775 | smp.boundingbox = path.boundingbox; 776 | smp.holechildren = path.holechildren; 777 | smp.isholepath = path.isholepath; 778 | 779 | while (pcnt < path.points.length) { 780 | // 5.1. Find sequences of points with only 2 segment types 781 | segtype1 = path.points[pcnt].linesegment; segtype2 = -1; seqend = pcnt + 1; 782 | while ( 783 | ((path.points[seqend].linesegment === segtype1) || (path.points[seqend].linesegment === segtype2) || (segtype2 === -1)) && 784 | (seqend < path.points.length - 1)) { 785 | if ((path.points[seqend].linesegment !== segtype1) && (segtype2 === -1)) { 786 | segtype2 = path.points[seqend].linesegment; 787 | } 788 | seqend++; 789 | } 790 | if (seqend === path.points.length - 1) { 791 | seqend = 0; 792 | } 793 | 794 | // 5.2. - 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences 795 | smp.segments = smp.segments.concat(_this.fitseq(path, ltres, qtres, pcnt, seqend)); 796 | 797 | // forward pcnt; 798 | if (seqend > 0) { 799 | pcnt = seqend; 800 | } else { 801 | pcnt = path.points.length; 802 | } 803 | }// End of pcnt loop 804 | return smp; 805 | }, // End of tracepath() 806 | 807 | // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes, 808 | // called from tracepath() 809 | this.fitseq = function(path, ltres, qtres, seqstart, seqend) { 810 | // return if invalid seqend 811 | if ((seqend > path.points.length) || (seqend < 0)) { 812 | return []; 813 | } 814 | // variables 815 | var errorpoint = seqstart, errorval = 0, curvepass = true, px, py, dist2; 816 | var tl = (seqend - seqstart); if (tl < 0) { 817 | tl += path.points.length; 818 | } 819 | var vx = (path.points[seqend].x - path.points[seqstart].x) / tl, 820 | vy = (path.points[seqend].y - path.points[seqstart].y) / tl; 821 | 822 | // 5.2. Fit a straight line on the sequence 823 | var pcnt = (seqstart + 1) % path.points.length, pl; 824 | while (pcnt !== seqend) { 825 | pl = pcnt - seqstart; if (pl < 0) { 826 | pl += path.points.length; 827 | } 828 | px = path.points[seqstart].x + vx * pl; py = path.points[seqstart].y + vy * pl; 829 | dist2 = (path.points[pcnt].x - px) * (path.points[pcnt].x - px) + (path.points[pcnt].y - py) * (path.points[pcnt].y - py); 830 | if (dist2 > ltres) { 831 | curvepass = false; 832 | } 833 | if (dist2 > errorval) { 834 | errorpoint = pcnt; errorval = dist2; 835 | } 836 | pcnt = (pcnt + 1) % path.points.length; 837 | } 838 | // return straight line if fits 839 | if (curvepass) { 840 | return [{ 841 | type: "L", x1: path.points[seqstart].x, y1: path.points[seqstart].y, x2: path.points[seqend].x, y2: path.points[seqend].y, 842 | }]; 843 | } 844 | 845 | // 5.3. If the straight line fails (distance error>ltres), find the point with the biggest error 846 | var fitpoint = errorpoint; curvepass = true; errorval = 0; 847 | 848 | // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence 849 | // helpers and projecting to get control point 850 | var t = (fitpoint - seqstart) / tl, t1 = (1 - t) * (1 - t), t2 = 2 * (1 - t) * t, t3 = t * t; 851 | var cpx = (t1 * path.points[seqstart].x + t3 * path.points[seqend].x - path.points[fitpoint].x) / -t2, 852 | cpy = (t1 * path.points[seqstart].y + t3 * path.points[seqend].y - path.points[fitpoint].y) / -t2; 853 | 854 | // Check every point 855 | pcnt = seqstart + 1; 856 | while (pcnt !== seqend) { 857 | t = (pcnt - seqstart) / tl; t1 = (1 - t) * (1 - t); t2 = 2 * (1 - t) * t; t3 = t * t; 858 | px = t1 * path.points[seqstart].x + t2 * cpx + t3 * path.points[seqend].x; 859 | py = t1 * path.points[seqstart].y + t2 * cpy + t3 * path.points[seqend].y; 860 | 861 | dist2 = (path.points[pcnt].x - px) * (path.points[pcnt].x - px) + (path.points[pcnt].y - py) * (path.points[pcnt].y - py); 862 | 863 | if (dist2 > qtres) { 864 | curvepass = false; 865 | } 866 | if (dist2 > errorval) { 867 | errorpoint = pcnt; errorval = dist2; 868 | } 869 | pcnt = (pcnt + 1) % path.points.length; 870 | } 871 | // return spline if fits 872 | if (curvepass) { 873 | return [{ 874 | type: "Q", x1: path.points[seqstart].x, y1: path.points[seqstart].y, x2: cpx, y2: cpy, x3: path.points[seqend].x, y3: path.points[seqend].y, 875 | }]; 876 | } 877 | // 5.5. If the spline fails (distance error>qtres), find the point with the biggest error 878 | var splitpoint = fitpoint; // Earlier: Math.floor((fitpoint + errorpoint)/2); 879 | 880 | // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences 881 | return _this.fitseq(path, ltres, qtres, seqstart, splitpoint).concat( 882 | _this.fitseq(path, ltres, qtres, splitpoint, seqend)); 883 | }, // End of fitseq() 884 | 885 | // 5. Batch tracing paths 886 | this.batchtracepaths = function(internodepaths, ltres, qtres) { 887 | var btracedpaths = []; 888 | for (var k in internodepaths) { 889 | if (!internodepaths.hasOwnProperty(k)) { 890 | continue; 891 | } 892 | btracedpaths.push(_this.tracepath(internodepaths[k], ltres, qtres)); 893 | } 894 | return btracedpaths; 895 | }, 896 | 897 | // 5. Batch tracing layers 898 | this.batchtracelayers = function(binternodes, ltres, qtres) { 899 | var btbis = []; 900 | for (var k in binternodes) { 901 | if (!binternodes.hasOwnProperty(k)) { 902 | continue; 903 | } 904 | btbis[k] = _this.batchtracepaths(binternodes[k], ltres, qtres); 905 | } 906 | return btbis; 907 | }, 908 | 909 | // 910 | // SVG Drawing functions 911 | // 912 | 913 | // Rounding to given decimals https://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-in-javascript 914 | this.roundtodec = function(val, places) { 915 | return +val.toFixed(places); 916 | }, 917 | 918 | // Getting SVG path element string from a traced path 919 | this.svgpathstring = function(tracedata, lnum, pathnum, options) { 920 | var layer = tracedata.layers[lnum], smp = layer[pathnum], str = "", pcnt; 921 | 922 | // Line filter 923 | if (options.linefilter && (smp.segments.length < 3)) { 924 | return str; 925 | } 926 | 927 | // Starting path element, desc contains layer and path number 928 | if (!options.bw) { 929 | str = "= 0; pcnt--) { 973 | str += hsmp.segments[pcnt].type + " "; 974 | if (hsmp.segments[pcnt].hasOwnProperty("x3")) { 975 | str += hsmp.segments[pcnt].x2 * options.scale + " " + hsmp.segments[pcnt].y2 * options.scale + " "; 976 | } 977 | 978 | str += hsmp.segments[pcnt].x1 * options.scale + " " + hsmp.segments[pcnt].y1 * options.scale + " "; 979 | } 980 | } else { 981 | if (hsmp.segments[hsmp.segments.length - 1].hasOwnProperty("x3")) { 982 | str += "M " + _this.roundtodec(hsmp.segments[hsmp.segments.length - 1].x3 * options.scale) + " " + _this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y3 * options.scale) + " "; 983 | } else { 984 | str += "M " + _this.roundtodec(hsmp.segments[hsmp.segments.length - 1].x2 * options.scale) + " " + _this.roundtodec(hsmp.segments[hsmp.segments.length - 1].y2 * options.scale) + " "; 985 | } 986 | 987 | for (pcnt = hsmp.segments.length - 1; pcnt >= 0; pcnt--) { 988 | str += hsmp.segments[pcnt].type + " "; 989 | if (hsmp.segments[pcnt].hasOwnProperty("x3")) { 990 | str += _this.roundtodec(hsmp.segments[pcnt].x2 * options.scale) + " " + _this.roundtodec(hsmp.segments[pcnt].y2 * options.scale) + " "; 991 | } 992 | str += _this.roundtodec(hsmp.segments[pcnt].x1 * options.scale) + " " + _this.roundtodec(hsmp.segments[pcnt].y1 * options.scale) + " "; 993 | } 994 | }// End of creating hole path string 995 | 996 | str += "Z "; // Close path 997 | }// End of holepath check 998 | 999 | // Closing path element 1000 | str += "\" />"; 1001 | 1002 | // Rendering control points 1003 | if (options.lcpr || options.qcpr) { 1004 | for (pcnt = 0; pcnt < smp.segments.length; pcnt++) { 1005 | if (smp.segments[pcnt].hasOwnProperty("x3") && options.qcpr) { 1006 | str += ""; 1007 | str += ""; 1008 | str += ""; 1009 | str += ""; 1010 | } 1011 | if ((!smp.segments[pcnt].hasOwnProperty("x3")) && options.lcpr) { 1012 | str += ""; 1013 | } 1014 | } 1015 | 1016 | // Hole children control points 1017 | for (var hcnt = 0; hcnt < smp.holechildren.length; hcnt++) { 1018 | var hsmp = layer[smp.holechildren[hcnt]]; 1019 | for (pcnt = 0; pcnt < hsmp.segments.length; pcnt++) { 1020 | if (hsmp.segments[pcnt].hasOwnProperty("x3") && options.qcpr) { 1021 | str += ""; 1022 | str += ""; 1023 | str += ""; 1024 | str += ""; 1025 | } 1026 | if ((!hsmp.segments[pcnt].hasOwnProperty("x3")) && options.lcpr) { 1027 | str += ""; 1028 | } 1029 | } 1030 | } 1031 | }// End of Rendering control points 1032 | return str; 1033 | }, // End of svgpathstring() 1034 | 1035 | // Converting tracedata to an SVG string 1036 | this.getsvgstring = function(tracedata, options) { 1037 | options = _this.checkoptions(options); 1038 | 1039 | const w = tracedata.width * options.scale; 1040 | const h = tracedata.height * options.scale; 1041 | 1042 | // SVG start 1043 | let svgstr = ""; 1045 | 1046 | // Drawing: Layers and Paths loops 1047 | for (let lcnt = 0; lcnt < tracedata.layers.length; lcnt++) { 1048 | for (let pcnt = 0; pcnt < tracedata.layers[lcnt].length; pcnt++) { 1049 | // Adding SVG string 1050 | if (!tracedata.layers[lcnt][pcnt].isholepath) { 1051 | if (!options.bw) { 1052 | svgstr += _this.svgpathstring(tracedata, lcnt, pcnt, options); 1053 | } else { 1054 | if (tracedata.palette[lcnt].r + tracedata.palette[lcnt].g + tracedata.palette[lcnt].b < 255) { 1055 | svgstr += _this.svgpathstring(tracedata, lcnt, pcnt, options); 1056 | } 1057 | } 1058 | } 1059 | }// End of paths loop 1060 | }// End of layers loop 1061 | 1062 | // SVG End 1063 | svgstr += ""; 1064 | 1065 | return svgstr; 1066 | }, // End of getsvgstring() 1067 | 1068 | // Comparator for numeric Array.sort 1069 | this.compareNumbers = function(a, b) { 1070 | return a - b; 1071 | }, 1072 | 1073 | // Convert color object to rgba string 1074 | this.torgbastr = function(c) { 1075 | return "rgba(" + c.r + "," + c.g + "," + c.b + "," + c.a + ")"; 1076 | }, 1077 | 1078 | // Convert color object to SVG color string 1079 | this.tosvgcolorstr = function(c, options) { 1080 | return "fill=\"rgb(" + c.r + "," + c.g + "," + c.b + ")\" stroke=\"rgb(" + c.r + "," + c.g + "," + c.b + ")\" stroke-width=\"" + options.strokewidth + "\" opacity=\"" + c.a / 255.0 + "\" "; 1081 | }, 1082 | 1083 | // Helper function: Appending an element to a container from an svgstring 1084 | this.appendSVGString = function(svgstr, parentid) { 1085 | let div; 1086 | if (parentid) { 1087 | div = document.getElementById(parentid); 1088 | if (!div) { 1089 | div = document.createElement("div"); 1090 | div.id = parentid; 1091 | document.body.appendChild(div); 1092 | } 1093 | } else { 1094 | div = document.createElement("div"); 1095 | document.body.appendChild(div); 1096 | } 1097 | div.innerHTML += svgstr; 1098 | }, 1099 | 1100 | // 1101 | // Canvas functions 1102 | // 1103 | 1104 | // Gaussian kernels for blur 1105 | this.gks = [ 1106 | [0.27901, 0.44198, 0.27901], 1107 | [0.135336, 0.228569, 0.272192, 0.228569, 0.135336], 1108 | [0.086776, 0.136394, 0.178908, 0.195843, 0.178908, 0.136394, 0.086776], 1109 | [0.063327, 0.093095, 0.122589, 0.144599, 0.152781, 0.144599, 0.122589, 0.093095, 0.063327], 1110 | [0.049692, 0.069304, 0.089767, 0.107988, 0.120651, 0.125194, 0.120651, 0.107988, 0.089767, 0.069304, 0.049692], 1111 | ], 1112 | 1113 | // Selective Gaussian blur for preprocessing 1114 | this.blur = function(imgd, radius, delta) { 1115 | let i; 1116 | let j; 1117 | let k; 1118 | let d; 1119 | let idx; 1120 | let racc; 1121 | let gacc; 1122 | let bacc; 1123 | let aacc; 1124 | let wacc; 1125 | 1126 | // new ImageData 1127 | const imgd2 = { 1128 | width: imgd.width, height: imgd.height, data: [], 1129 | }; 1130 | 1131 | // radius and delta limits, this kernel 1132 | radius = Math.floor(radius); if (radius < 1) { 1133 | return imgd; 1134 | } if (radius > 5) { 1135 | radius = 5; 1136 | } delta = Math.abs(delta); if (delta > 1024) { 1137 | delta = 1024; 1138 | } 1139 | const thisgk = _this.gks[radius - 1]; 1140 | 1141 | // loop through all pixels, horizontal blur 1142 | for (j = 0; j < imgd.height; j++) { 1143 | for (i = 0; i < imgd.width; i++) { 1144 | racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; 1145 | // gauss kernel loop 1146 | for (k = -radius; k < radius + 1; k++) { 1147 | // add weighted color values 1148 | if ((i + k > 0) && (i + k < imgd.width)) { 1149 | idx = (j * imgd.width + i + k) * 4; 1150 | racc += imgd.data[idx] * thisgk[k + radius]; 1151 | gacc += imgd.data[idx + 1] * thisgk[k + radius]; 1152 | bacc += imgd.data[idx + 2] * thisgk[k + radius]; 1153 | aacc += imgd.data[idx + 3] * thisgk[k + radius]; 1154 | wacc += thisgk[k + radius]; 1155 | } 1156 | } 1157 | // The new pixel 1158 | idx = (j * imgd.width + i) * 4; 1159 | imgd2.data[idx] = Math.floor(racc / wacc); 1160 | imgd2.data[idx + 1] = Math.floor(gacc / wacc); 1161 | imgd2.data[idx + 2] = Math.floor(bacc / wacc); 1162 | imgd2.data[idx + 3] = Math.floor(aacc / wacc); 1163 | }// End of width loop 1164 | }// End of horizontal blur 1165 | 1166 | // copying the half blurred imgd2 1167 | const himgd = new Uint8ClampedArray(imgd2.data); 1168 | 1169 | // loop through all pixels, vertical blur 1170 | for (j = 0; j < imgd.height; j++) { 1171 | for (i = 0; i < imgd.width; i++) { 1172 | racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0; 1173 | // gauss kernel loop 1174 | for (k = -radius; k < radius + 1; k++) { 1175 | // add weighted color values 1176 | if ((j + k > 0) && (j + k < imgd.height)) { 1177 | idx = ((j + k) * imgd.width + i) * 4; 1178 | racc += himgd[idx] * thisgk[k + radius]; 1179 | gacc += himgd[idx + 1] * thisgk[k + radius]; 1180 | bacc += himgd[idx + 2] * thisgk[k + radius]; 1181 | aacc += himgd[idx + 3] * thisgk[k + radius]; 1182 | wacc += thisgk[k + radius]; 1183 | } 1184 | } 1185 | // The new pixel 1186 | idx = (j * imgd.width + i) * 4; 1187 | imgd2.data[idx] = Math.floor(racc / wacc); 1188 | imgd2.data[idx + 1] = Math.floor(gacc / wacc); 1189 | imgd2.data[idx + 2] = Math.floor(bacc / wacc); 1190 | imgd2.data[idx + 3] = Math.floor(aacc / wacc); 1191 | }// End of width loop 1192 | }// End of vertical blur 1193 | // Selective blur: loop through all pixels 1194 | for (j = 0; j < imgd.height; j++) { 1195 | for (i = 0; i < imgd.width; i++) { 1196 | idx = (j * imgd.width + i) * 4; 1197 | // d is the difference between the blurred and the original pixel 1198 | d = Math.abs(imgd2.data[idx] - imgd.data[idx]) + Math.abs(imgd2.data[idx + 1] - imgd.data[idx + 1]) + 1199 | Math.abs(imgd2.data[idx + 2] - imgd.data[idx + 2]) + Math.abs(imgd2.data[idx + 3] - imgd.data[idx + 3]); 1200 | // selective blur: if d>delta, put the original pixel back 1201 | if (d > delta) { 1202 | imgd2.data[idx] = imgd.data[idx]; 1203 | imgd2.data[idx + 1] = imgd.data[idx + 1]; 1204 | imgd2.data[idx + 2] = imgd.data[idx + 2]; 1205 | imgd2.data[idx + 3] = imgd.data[idx + 3]; 1206 | } 1207 | } 1208 | }// End of Selective blur 1209 | 1210 | return imgd2; 1211 | }, // End of blur() 1212 | 1213 | // Helper function: loading an image from a URL, then executing callback with canvas as argument 1214 | this.loadImage = function(url, callback, options) { 1215 | const img = new Image(); 1216 | if (options && options.corsenabled) { 1217 | img.crossOrigin = "Anonymous"; 1218 | } 1219 | img.onload = function() { 1220 | const canvas = document.createElement("canvas"); 1221 | canvas.width = img.width; 1222 | canvas.height = img.height; 1223 | const context = canvas.getContext("2d"); 1224 | context.drawImage(img, 0, 0); 1225 | callback(canvas); 1226 | }; 1227 | img.src = url; 1228 | }, 1229 | 1230 | // Helper function: getting ImageData from a canvas 1231 | this.getImgdata = function(canvas) { 1232 | const context = canvas.getContext("2d"); 1233 | return context.getImageData(0, 0, canvas.width, canvas.height); 1234 | }, 1235 | 1236 | // Special palette to use with drawlayers() 1237 | this.specpalette = [ 1238 | { 1239 | r: 0, g: 0, b: 0, a: 255, 1240 | }, { 1241 | r: 128, g: 128, b: 128, a: 255, 1242 | }, { 1243 | r: 0, g: 0, b: 128, a: 255, 1244 | }, { 1245 | r: 64, g: 64, b: 128, a: 255, 1246 | }, 1247 | { 1248 | r: 192, g: 192, b: 192, a: 255, 1249 | }, { 1250 | r: 255, g: 255, b: 255, a: 255, 1251 | }, { 1252 | r: 128, g: 128, b: 192, a: 255, 1253 | }, { 1254 | r: 0, g: 0, b: 192, a: 255, 1255 | }, 1256 | { 1257 | r: 128, g: 0, b: 0, a: 255, 1258 | }, { 1259 | r: 128, g: 64, b: 64, a: 255, 1260 | }, { 1261 | r: 128, g: 0, b: 128, a: 255, 1262 | }, { 1263 | r: 168, g: 168, b: 168, a: 255, 1264 | }, 1265 | { 1266 | r: 192, g: 128, b: 128, a: 255, 1267 | }, { 1268 | r: 192, g: 0, b: 0, a: 255, 1269 | }, { 1270 | r: 255, g: 255, b: 255, a: 255, 1271 | }, { 1272 | r: 0, g: 128, b: 0, a: 255, 1273 | }, 1274 | ], 1275 | 1276 | // Helper function: Drawing all edge node layers into a container 1277 | this.drawLayers = function(layers, palette, scale, parentid) { 1278 | scale = scale || 1; 1279 | let w; 1280 | let h; 1281 | let i; 1282 | let j; 1283 | let k; 1284 | 1285 | // Preparing container 1286 | let div; 1287 | if (parentid) { 1288 | div = document.getElementById(parentid); 1289 | if (!div) { 1290 | div = document.createElement("div"); 1291 | div.id = parentid; 1292 | document.body.appendChild(div); 1293 | } 1294 | } else { 1295 | div = document.createElement("div"); 1296 | document.body.appendChild(div); 1297 | } 1298 | 1299 | // Layers loop 1300 | for (k in layers) { 1301 | if (!layers.hasOwnProperty(k)) { 1302 | continue; 1303 | } 1304 | 1305 | // width, height 1306 | w = layers[k][0].length; h = layers[k].length; 1307 | 1308 | // Creating new canvas for every layer 1309 | const canvas = document.createElement("canvas"); canvas.width = w * scale; canvas.height = h * scale; 1310 | const context = canvas.getContext("2d"); 1311 | 1312 | // Drawing 1313 | for (j = 0; j < h; j++) { 1314 | for (i = 0; i < w; i++) { 1315 | context.fillStyle = _this.torgbastr(palette[layers[k][j][i] % palette.length]); 1316 | context.fillRect(i * scale, j * scale, scale, scale); 1317 | } 1318 | } 1319 | 1320 | // Appending canvas to container 1321 | div.appendChild(canvas); 1322 | }// End of Layers loop 1323 | }// End of drawlayers 1324 | 1325 | ;// End of function list 1326 | }// End of ImageTracer object 1327 | 1328 | // export as AMD module / Node module / browser or worker variable 1329 | if (typeof define === "function" && define.amd) { 1330 | define(function() { 1331 | return new ImageTracer(); 1332 | }); 1333 | } else if (typeof module !== "undefined") { 1334 | module.exports = new ImageTracer(); 1335 | } else if (typeof self !== "undefined") { 1336 | self.ImageTracer = new ImageTracer(); 1337 | } else window.ImageTracer = new ImageTracer(); 1338 | })(); 1339 | --------------------------------------------------------------------------------