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 |
15 |
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 |
31 |
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 |
64 |
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'"
139 | )
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](../../)
2 | [](../../issues)
3 | [](/LICENSE.md)
4 | [](../../commits/master)
5 | [](../../commits/master)
6 | [](https://pypistats.org/packages/svgtrace)
7 | [](https://pepy.tech/project/svgtrace)
8 | [](https://pypi.org/project/svgtrace)
9 |
10 |
11 | # SvgTrace
12 |
13 |
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 = "";
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