diff --git a/envs/dev.yaml b/envs/dev.yaml
index b165c52..9fdcb8f 100644
--- a/envs/dev.yaml
+++ b/envs/dev.yaml
@@ -5,90 +5,91 @@ dependencies:
- _libgcc_mutex=0.1=main
- _openmp_mutex=5.1=1_gnu
- bzip2=1.0.8=h5eee18b_6
- - ca-certificates=2024.7.2=h06a4308_0
- - ld_impl_linux-64=2.38=h1181459_1
+ - ca-certificates=2025.2.25=h06a4308_0
+ - ld_impl_linux-64=2.40=h12ee557_0
- libffi=3.4.4=h6a678d5_1
- libgcc-ng=11.2.0=h1234567_1
- libgomp=11.2.0=h1234567_1
- libstdcxx-ng=11.2.0=h1234567_1
- libuuid=1.41.5=h5eee18b_0
- ncurses=6.4=h6a678d5_0
- - openssl=3.0.14=h5eee18b_0
- - pip=24.2=py311h06a4308_0
+ - openssl=3.0.16=h5eee18b_0
+ - pip=25.1=pyhc872135_2
- python=3.11.5=h955ad1f_0
- readline=8.2=h5eee18b_0
- - setuptools=72.1.0=py311h06a4308_0
+ - setuptools=78.1.1=py311h06a4308_0
- sqlite=3.45.3=h5eee18b_0
- tk=8.6.14=h39e8969_0
- - tzdata=2024a=h04d1e81_0
- - xz=5.4.6=h5eee18b_1
+ - tzdata=2025b=h04d1e81_0
+ - wheel=0.45.1=py311h06a4308_0
+ - xz=5.6.4=h5eee18b_1
- zlib=1.2.13=h5eee18b_1
- pip:
- - blinker==1.8.2
- - boto3==1.35.2
- - botocore==1.35.2
- - build==1.2.1
+ - blinker==1.9.0
+ - boto3==1.35.99
+ - botocore==1.35.99
+ - build==1.2.2.post1
- cairocffi==1.7.1
- cairosvg==2.7.1
- - certifi==2024.7.4
- - cffi==1.17.0
+ - certifi==2025.4.26
+ - cffi==1.17.1
- cfgv==3.4.0
- - charset-normalizer==3.3.2
- - click==8.1.7
- - cssselect2==0.7.0
+ - charset-normalizer==3.4.2
+ - click==8.2.1
+ - cssselect2==0.8.0
- cuid==0.4
- defusedxml==0.7.1
- - distlib==0.3.8
- - exceptiongroup==1.2.2
+ - distlib==0.3.9
+ - exceptiongroup==1.3.0
- faiss-cpu==1.7.4
- - filelock==3.15.4
+ - filelock==3.18.0
- flask==2.3.3
- - flask-cors==4.0.1
+ - flask-cors==4.0.2
- gunicorn==21.2.0
- - identify==2.6.0
- - idna==3.7
- - imageio==2.35.1
- - iniconfig==2.0.0
+ - identify==2.6.12
+ - idna==3.10
+ - imageio==2.37.0
+ - iniconfig==2.1.0
- itsdangerous==2.2.0
- - jinja2==3.1.4
+ - jinja2==3.1.6
- jmespath==1.0.1
- lazy-loader==0.4
- llvmlite==0.40.1
- - markupsafe==2.1.5
- - networkx==3.3
+ - markupsafe==3.0.2
+ - networkx==3.4.2
- nodeenv==1.9.1
- numba==0.57.1
- numpy==1.24.4
- opencv-python-headless==4.8.0.76
- - packaging==24.1
+ - packaging==25.0
- pillow==10.0.1
- pip-tools==7.4.1
- - platformdirs==4.2.2
- - pluggy==1.5.0
- - pre-commit==3.8.0
- - pyclipper==1.3.0.post5
+ - platformdirs==4.3.8
+ - pluggy==1.6.0
+ - pre-commit==4.2.0
- pycparser==2.22
- pypotrace==0.3
- - pyproject-hooks==1.1.0
- - pytest==8.3.2
+ - pyproject-hooks==1.2.0
+ - pytest==8.3.5
- pytest-dotenv==0.5.2
- python-dateutil==2.9.0.post0
- - python-dotenv==1.0.1
- - pywavelets==1.7.0
+ - python-dotenv==1.1.0
+ - pywavelets==1.8.0
- pyyaml==6.0.2
- requests==2.31.0
- - s3transfer==0.10.2
+ - s3transfer==0.10.4
- scikit-image==0.21.0
- - scipy==1.14.1
+ - scipy==1.15.3
- sentry-sdk==1.30.0
- sewar==0.4.6
- - six==1.16.0
- - tifffile==2024.8.10
- - tinycss2==1.3.0
- - tomli==2.0.1
- - urllib3==2.2.2
- - virtualenv==20.26.3
+ - six==1.17.0
+ - skia-pathops==0.8.0.post2
+ - tifffile==2025.5.10
+ - tinycss2==1.4.0
+ - tomli==2.2.1
+ - typing-extensions==4.13.2
+ - urllib3==2.4.0
+ - virtualenv==20.31.2
- webencodings==0.5.1
- - werkzeug==3.0.3
- - wheel==0.44.0
+ - werkzeug==3.1.3
prefix: /opt/conda/envs/dev
diff --git a/envs/prod.yaml b/envs/prod.yaml
index 792ddd0..f7eae44 100644
--- a/envs/prod.yaml
+++ b/envs/prod.yaml
@@ -5,62 +5,62 @@ dependencies:
- _libgcc_mutex=0.1=main
- _openmp_mutex=5.1=1_gnu
- bzip2=1.0.8=h5eee18b_6
- - ca-certificates=2024.7.2=h06a4308_0
- - ld_impl_linux-64=2.38=h1181459_1
+ - ca-certificates=2025.2.25=h06a4308_0
+ - ld_impl_linux-64=2.40=h12ee557_0
- libffi=3.4.4=h6a678d5_1
- libgcc-ng=11.2.0=h1234567_1
- libgomp=11.2.0=h1234567_1
- libstdcxx-ng=11.2.0=h1234567_1
- libuuid=1.41.5=h5eee18b_0
- ncurses=6.4=h6a678d5_0
- - openssl=3.0.14=h5eee18b_0
- - pip=24.2=py311h06a4308_0
+ - openssl=3.0.16=h5eee18b_0
+ - pip=25.1=pyhc872135_2
- python=3.11.5=h955ad1f_0
- readline=8.2=h5eee18b_0
- - setuptools=72.1.0=py311h06a4308_0
+ - setuptools=78.1.1=py311h06a4308_0
- sqlite=3.45.3=h5eee18b_0
- tk=8.6.14=h39e8969_0
- - tzdata=2024a=h04d1e81_0
- - wheel=0.43.0=py311h06a4308_0
- - xz=5.4.6=h5eee18b_1
+ - tzdata=2025b=h04d1e81_0
+ - wheel=0.45.1=py311h06a4308_0
+ - xz=5.6.4=h5eee18b_1
- zlib=1.2.13=h5eee18b_1
- pip:
- - blinker==1.8.2
- - boto3==1.35.2
- - botocore==1.35.2
- - certifi==2024.7.4
- - charset-normalizer==3.3.2
- - click==8.1.7
+ - blinker==1.9.0
+ - boto3==1.35.99
+ - botocore==1.35.99
+ - certifi==2025.4.26
+ - charset-normalizer==3.4.2
+ - click==8.2.1
- cuid==0.4
- faiss-cpu==1.7.4
- flask==2.3.3
- - flask-cors==4.0.1
+ - flask-cors==4.0.2
- gunicorn==21.2.0
- - idna==3.7
- - imageio==2.35.1
+ - idna==3.10
+ - imageio==2.37.0
- itsdangerous==2.2.0
- - jinja2==3.1.4
+ - jinja2==3.1.6
- jmespath==1.0.1
- lazy-loader==0.4
- llvmlite==0.40.1
- - markupsafe==2.1.5
- - networkx==3.3
+ - markupsafe==3.0.2
+ - networkx==3.4.2
- numba==0.57.1
- numpy==1.24.4
- opencv-python-headless==4.8.0.76
- - packaging==24.1
+ - packaging==25.0
- pillow==10.0.1
- - pyclipper==1.3.0.post5
- pypotrace==0.3
- python-dateutil==2.9.0.post0
- - pywavelets==1.7.0
+ - pywavelets==1.8.0
- requests==2.31.0
- - s3transfer==0.10.2
+ - s3transfer==0.10.4
- scikit-image==0.21.0
- - scipy==1.14.1
+ - scipy==1.15.3
- sentry-sdk==1.30.0
- - six==1.16.0
- - tifffile==2024.8.10
- - urllib3==2.2.2
- - werkzeug==3.0.3
+ - six==1.17.0
+ - skia-pathops==0.8.0.post2
+ - tifffile==2025.5.10
+ - urllib3==2.4.0
+ - werkzeug==3.1.3
prefix: /opt/conda/envs/prod
diff --git a/requirements/dev.in b/requirements/dev.in
index 0fa3f0c..ff7431e 100644
--- a/requirements/dev.in
+++ b/requirements/dev.in
@@ -7,4 +7,4 @@ pre-commit
pip-tools
pytest
cairosvg~=2.7.1
-sewar~=0.4.6
+sewar~=0.4.6
\ No newline at end of file
diff --git a/requirements/dev.txt b/requirements/dev.txt
index a60d12c..d9c7b5f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -4,112 +4,112 @@
#
# pip-compile /workspaces/vectorizing/scripts/../requirements/dev.in
#
-blinker==1.8.2
+blinker==1.9.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask
-boto3==1.35.2
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-botocore==1.35.2
+boto3==1.35.99
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+botocore==1.35.99
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# boto3
# s3transfer
-build==1.2.1
+build==1.2.2.post1
# via pip-tools
cairocffi==1.7.1
# via cairosvg
cairosvg==2.7.1
# via -r /workspaces/vectorizing/scripts/../requirements/dev.in
-certifi==2024.7.4
+certifi==2025.4.26
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# requests
# sentry-sdk
-cffi==1.17.0
+cffi==1.17.1
# via cairocffi
cfgv==3.4.0
# via pre-commit
-charset-normalizer==3.3.2
+charset-normalizer==3.4.2
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# requests
-click==8.1.7
+click==8.2.1
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask
# pip-tools
-cssselect2==0.7.0
+cssselect2==0.8.0
# via cairosvg
cuid==0.4
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # via -r /workspaces/vectorizing/requirements/prod.txt
defusedxml==0.7.1
# via cairosvg
-distlib==0.3.8
+distlib==0.3.9
# via virtualenv
-exceptiongroup==1.2.2
+exceptiongroup==1.3.0
# via pytest
faiss-cpu==1.7.4
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-filelock==3.15.4
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+filelock==3.18.0
# via virtualenv
flask==2.3.3
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask-cors
-flask-cors==4.0.1
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+flask-cors==4.0.2
+ # via -r /workspaces/vectorizing/requirements/prod.txt
gunicorn==21.2.0
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-identify==2.6.0
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+identify==2.6.12
# via pre-commit
-idna==3.7
+idna==3.10
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# requests
-imageio==2.35.1
+imageio==2.37.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
-iniconfig==2.0.0
+iniconfig==2.1.0
# via pytest
itsdangerous==2.2.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask
-jinja2==3.1.4
+jinja2==3.1.6
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask
jmespath==1.0.1
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# boto3
# botocore
lazy-loader==0.4
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
llvmlite==0.40.1
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# numba
-markupsafe==2.1.5
+markupsafe==3.0.2
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# jinja2
# werkzeug
-networkx==3.3
+networkx==3.4.2
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
nodeenv==1.9.1
# via pre-commit
numba==0.57.1
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # via -r /workspaces/vectorizing/requirements/prod.txt
numpy==1.24.4
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# imageio
# numba
# opencv-python-headless
@@ -120,10 +120,10 @@ numpy==1.24.4
# sewar
# tifffile
opencv-python-headless==4.8.0.76
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-packaging==24.1
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+packaging==25.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# build
# gunicorn
# lazy-loader
@@ -131,30 +131,28 @@ packaging==24.1
# scikit-image
pillow==10.0.1
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# cairosvg
# imageio
# scikit-image
# sewar
pip-tools==7.4.1
# via -r /workspaces/vectorizing/scripts/../requirements/dev.in
-platformdirs==4.2.2
+platformdirs==4.3.8
# via virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
-pre-commit==3.8.0
+pre-commit==4.2.0
# via -r /workspaces/vectorizing/scripts/../requirements/dev.in
-pyclipper==1.3.0.post5
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
pycparser==2.22
# via cffi
pypotrace==0.3
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-pyproject-hooks==1.1.0
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+pyproject-hooks==1.2.0
# via
# build
# pip-tools
-pytest==8.3.2
+pytest==8.3.5
# via
# -r /workspaces/vectorizing/scripts/../requirements/dev.in
# pytest-dotenv
@@ -162,67 +160,71 @@ pytest-dotenv==0.5.2
# via -r /workspaces/vectorizing/scripts/../requirements/dev.in
python-dateutil==2.9.0.post0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# botocore
-python-dotenv==1.0.1
+python-dotenv==1.1.0
# via pytest-dotenv
-pywavelets==1.7.0
+pywavelets==1.8.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
pyyaml==6.0.2
# via pre-commit
requests==2.31.0
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-s3transfer==0.10.2
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+s3transfer==0.10.4
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# boto3
scikit-image==0.21.0
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
-scipy==1.14.1
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+scipy==1.15.3
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
# sewar
sentry-sdk==1.30.0
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # via -r /workspaces/vectorizing/requirements/prod.txt
sewar==0.4.6
# via -r /workspaces/vectorizing/scripts/../requirements/dev.in
-six==1.16.0
+six==1.17.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# python-dateutil
-tifffile==2024.8.10
+skia-pathops==0.8.0.post2
+ # via -r /workspaces/vectorizing/requirements/prod.txt
+tifffile==2025.5.10
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# scikit-image
-tinycss2==1.3.0
+tinycss2==1.4.0
# via
# cairosvg
# cssselect2
-tomli==2.0.1
+tomli==2.2.1
# via
# build
# pip-tools
# pytest
-urllib3==2.2.2
+typing-extensions==4.13.2
+ # via exceptiongroup
+urllib3==2.4.0
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# botocore
# requests
# sentry-sdk
-virtualenv==20.26.3
+virtualenv==20.31.2
# via pre-commit
webencodings==0.5.1
# via
# cssselect2
# tinycss2
-werkzeug==3.0.3
+werkzeug==3.1.3
# via
- # -r /workspaces/vectorizing/scripts/../requirements/prod.txt
+ # -r /workspaces/vectorizing/requirements/prod.txt
# flask
-wheel==0.44.0
+wheel==0.45.1
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/prod.in b/requirements/prod.in
index adca285..d5b6003 100644
--- a/requirements/prod.in
+++ b/requirements/prod.in
@@ -8,9 +8,9 @@ sentry-sdk~=1.30.0
numba~=0.57.1
pillow~=10.0.0
faiss-cpu~=1.7.4
-pyclipper~=1.3.0.post4
pypotrace~=0.3
scikit-image~=0.21.0
opencv-python-headless~=4.8.0.76
requests~=2.31.0
gunicorn~=21.2.0
+skia-pathops~=0.8.0
diff --git a/requirements/prod.txt b/requirements/prod.txt
index d6c739b..ce3be5e 100644
--- a/requirements/prod.txt
+++ b/requirements/prod.txt
@@ -4,21 +4,21 @@
#
# pip-compile /workspaces/vectorizing/scripts/../requirements/prod.in
#
-blinker==1.8.2
+blinker==1.9.0
# via flask
-boto3==1.35.2
+boto3==1.35.99
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-botocore==1.35.2
+botocore==1.35.99
# via
# boto3
# s3transfer
-certifi==2024.7.4
+certifi==2025.4.26
# via
# requests
# sentry-sdk
-charset-normalizer==3.3.2
+charset-normalizer==3.4.2
# via requests
-click==8.1.7
+click==8.2.1
# via flask
cuid==0.4
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
@@ -28,17 +28,17 @@ flask==2.3.3
# via
# -r /workspaces/vectorizing/scripts/../requirements/prod.in
# flask-cors
-flask-cors==4.0.1
+flask-cors==4.0.2
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
gunicorn==21.2.0
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-idna==3.7
+idna==3.10
# via requests
-imageio==2.35.1
+imageio==2.37.0
# via scikit-image
itsdangerous==2.2.0
# via flask
-jinja2==3.1.4
+jinja2==3.1.6
# via flask
jmespath==1.0.1
# via
@@ -48,11 +48,11 @@ lazy-loader==0.4
# via scikit-image
llvmlite==0.40.1
# via numba
-markupsafe==2.1.5
+markupsafe==3.0.2
# via
# jinja2
# werkzeug
-networkx==3.3
+networkx==3.4.2
# via scikit-image
numba==0.57.1
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
@@ -68,7 +68,7 @@ numpy==1.24.4
# tifffile
opencv-python-headless==4.8.0.76
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-packaging==24.1
+packaging==25.0
# via
# gunicorn
# lazy-loader
@@ -78,32 +78,32 @@ pillow==10.0.1
# -r /workspaces/vectorizing/scripts/../requirements/prod.in
# imageio
# scikit-image
-pyclipper==1.3.0.post5
- # via -r /workspaces/vectorizing/scripts/../requirements/prod.in
pypotrace==0.3
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
python-dateutil==2.9.0.post0
# via botocore
-pywavelets==1.7.0
+pywavelets==1.8.0
# via scikit-image
requests==2.31.0
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-s3transfer==0.10.2
+s3transfer==0.10.4
# via boto3
scikit-image==0.21.0
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-scipy==1.14.1
+scipy==1.15.3
# via scikit-image
sentry-sdk==1.30.0
# via -r /workspaces/vectorizing/scripts/../requirements/prod.in
-six==1.16.0
+six==1.17.0
# via python-dateutil
-tifffile==2024.8.10
+skia-pathops==0.8.0.post2
+ # via -r /workspaces/vectorizing/scripts/../requirements/prod.in
+tifffile==2025.5.10
# via scikit-image
-urllib3==2.2.2
+urllib3==2.4.0
# via
# botocore
# requests
# sentry-sdk
-werkzeug==3.0.3
+werkzeug==3.1.3
# via flask
diff --git a/vectorizing/__init__.py b/vectorizing/__init__.py
index bbd83ac..879184e 100644
--- a/vectorizing/__init__.py
+++ b/vectorizing/__init__.py
@@ -6,12 +6,12 @@
from vectorizing.server.timer import Timer
from vectorizing.server.logs import setup_logs
from vectorizing.server.s3 import upload_markup
-from vectorizing.svg.markup import create_markup
+from vectorizing.svg.markup import generate_SVG_markup
from vectorizing.util.read import try_read_image_from_url
from vectorizing.server.env import get_required, get_optional
from vectorizing.solvers.color.ColorSolver import ColorSolver
from vectorizing.solvers.binary.BinarySolver import BinarySolver
-from vectorizing.geometry.bounds import compound_path_list_bounds
+from vectorizing.geometry.bounds import compound_paths_bounds
# 0 -> BinarySolver
# 1 -> ColorSolver
@@ -35,8 +35,8 @@ def process_binary(img):
solver = BinarySolver(img)
return solver.solve()
-def process_color(img, color_count, tolerance, timer):
- solver = ColorSolver(img, color_count, tolerance, timer)
+def process_color(img, color_count, timer):
+ solver = ColorSolver(img, color_count, timer)
return solver.solve()
def validate_args(args):
@@ -56,17 +56,12 @@ def validate_args(args):
if not only_numbers:
return False
- tolerance = args.get("tolerance")
- if tolerance is not None and tolerance < 0:
- return False
-
return SimpleNamespace(
crop_box=box,
solver=solver,
url=args.get("url"),
raw=args.get("raw"),
- color_count=args.get("color_count"),
- tolerance=args.get("tolerance"),
+ color_count=args.get("color_count")
)
def invalid_args():
@@ -92,10 +87,6 @@ def index():
color_count = args.color_count
raw = args.raw
crop_box = args.crop_box
- tolerance = args.tolerance
-
- if tolerance is None:
- tolerance = 0.2
try:
timer = Timer()
@@ -114,13 +105,13 @@ def index():
else:
timer.start_timer('Color Solver - Total')
- solved = process_color(img, color_count, tolerance, timer)
+ solved = process_color(img, color_count, timer)
timer.end_timer()
compound_paths, colors, width, height = solved
timer.start_timer('Markup Creation')
- markup = create_markup(compound_paths, colors, width, height)
+ markup = generate_SVG_markup(compound_paths, colors, width, height)
timer.end_timer()
if raw:
@@ -131,7 +122,7 @@ def index():
timer.end_timer()
timer.start_timer('Bounds Creation')
- bounds = compound_path_list_bounds(compound_paths, tolerance)
+ bounds = compound_paths_bounds(compound_paths)
timer.end_timer()
app.logger.info(timer.timelog())
@@ -140,7 +131,7 @@ def index():
'success': True,
'objectId': cuid_str,
'info': {
- 'bounds': bounds.to_dict(),
+ 'bounds': bounds,
'image_width': width,
'image_height': height
}
diff --git a/vectorizing/geometry/bounds.py b/vectorizing/geometry/bounds.py
index 00dd99f..0931cc9 100644
--- a/vectorizing/geometry/bounds.py
+++ b/vectorizing/geometry/bounds.py
@@ -1,53 +1,34 @@
import numpy as np
-# Class that represents a bounds object
-class Bounds:
- def __init__(self, min_x, min_y, max_x, max_y):
- self.min_x = min_x
- self.min_y = min_y
- self.max_x = max_x
- self.max_y = max_y
-
- def to_dict(self):
- return {
- 'top': self.min_y,
- 'left': self.min_x,
- 'bottom': self.max_y,
- 'right': self.max_x,
- 'width': self.max_x - self.min_x,
- 'height': self.max_y - self.min_y
- }
-# Given a list of bounds, it will compute the total bounds.
-# Meaning, the tightest bounds that contain all the bounds in the list.
-def compute_total_bounds(bounds_list):
- min_x = [bounds.min_x for bounds in bounds_list]
- min_y = [bounds.min_y for bounds in bounds_list]
- max_x = [bounds.max_x for bounds in bounds_list]
- max_y = [bounds.max_y for bounds in bounds_list]
+# Calculates bounds of a list of compound paths
+def compound_paths_bounds(compound_paths):
+ """
+ Calculates the combined bounds of a list of compound paths.
- return Bounds(
- min(min_x),
- min(min_y),
- max(max_x),
- max(max_y)
- )
+ Parameters:
+ compound_paths: The list of compound paths (SKPath).
-# Calculates bounds of a path made out CubicBeziers and SegmentLists
-def path_bounds(path, tolerance):
- bounds = [item.bounds(tolerance) for item in path]
- return compute_total_bounds(bounds)
+ Returns:
+ An object containing combined bounds info.
+ """
+ min_x = np.inf
+ min_y = np.inf
+ max_x = -np.inf
+ max_y = -np.inf
-# Calculates bounds of a compound path, meaning a list of paths
-def compound_path_bounds(compound_path, tolerance):
- bounds = [path_bounds(path, tolerance) for path in compound_path]
- return compute_total_bounds(bounds)
+ for compound_path in compound_paths:
+ l, t, r, b = compound_path.bounds
+ min_x = min(min_x, l)
+ min_y = min(min_y, t)
+ max_x = max(max_x, r)
+ max_y = max(max_y, b)
-# Calculates bounds of a list of compound paths
-def compound_path_list_bounds(compound_path_list, tolerance):
- bounds = [
- compound_path_bounds(compound_path, tolerance)
- for compound_path in compound_path_list
- if len(compound_path) > 0
- ]
- return compute_total_bounds(bounds)
\ No newline at end of file
+ return {
+ "top": min_y,
+ "left": min_x,
+ "bottom": max_y,
+ "right": max_x,
+ "width": max_x - min_x,
+ "height": max_y - min_y,
+ }
diff --git a/vectorizing/geometry/cubic_bezier.py b/vectorizing/geometry/cubic_bezier.py
deleted file mode 100644
index 4490d6b..0000000
--- a/vectorizing/geometry/cubic_bezier.py
+++ /dev/null
@@ -1,121 +0,0 @@
-import numpy as np
-from vectorizing.geometry.segment_list import SegmentList
-from numba import njit
-
-# Flattening approach is inspired in
-# https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.86.162&rep=rep1&type=pdf
-
-@njit
-def is_flat_enough(points, tol = .2):
- p0 = points[0]
- p1 = points[1]
- p2 = points[2]
- p3 = points[3]
-
- ux = 3 * p1[0] - 2 * p0[0] - p3[0]
- uy = 3 * p1[1] - 2 * p0[1] - p3[1]
- vx = 3 * p2[0] - 2 * p3[0] - p0[0]
- vy = 3 * p2[1] - 2 * p3[1] - p0[1]
-
- ux = ux ** 2
- uy = uy ** 2
- vx = vx ** 2
- vy = vy ** 2
-
- if ux < vx:
- ux = vx
-
- if uy < vy:
- uy = vy
-
- tolerance = 16 * tol ** 2
- return ux + uy <= tolerance
-
-@njit
-def subdivide(points):
- p0 = points[0]
- p1 = points[1]
- p2 = points[2]
- p3 = points[3]
-
- l0 = p0
- r3 = p3
-
- l1 = (
- (p0[0] + p1[0]) / 2,
- (p0[1] + p1[1]) / 2
- )
-
- r2 = (
- (p2[0] + p3[0]) / 2,
- (p2[1] + p3[1]) / 2
- )
-
- m = (
- (p1[0] + p2[0]) / 2,
- (p1[1] + p2[1]) / 2
- )
-
- l2 = (
- (m[0] + l1[0]) / 2,
- (m[1] + l1[1]) / 2,
- )
-
- r1 = (
- (m[0] + r2[0]) / 2,
- (m[1] + r2[1]) / 2
- )
-
- j = (
- (l2[0] + r1[0]) / 2,
- (l2[1] + r1[1]) / 2
- )
-
- l3 = r0 = j
-
- return (
- (l0, l1, l2, l3),
- (r0, r1, r2, r3)
- )
-
-@njit
-def flatten(points, tolerance):
- stack = [points]
- flattened = []
-
- while len(stack):
- first = stack.pop()
-
- if is_flat_enough(first, tolerance):
- flattened.append(first[0])
- flattened.append(first[1])
-
- else:
- subdivision = subdivide(first)
- stack.append(subdivision[1])
- stack.append(subdivision[0])
-
- return flattened
-
-# Class to represent a cubic bezier
-class CubicBezier:
- def __init__(self, p0, p1, p2, p3):
- self.p0 = p0
- self.p1 = p1
- self.p2 = p2
- self.p3 = p3
-
- def scaled(self, s):
- return CubicBezier(
- self.p0 * s,
- self.p1 * s,
- self.p2 * s,
- self.p3 * s
- )
-
- def flattened(self, tolerance):
- points = (tuple(self.p0), tuple(self.p1), tuple(self.p2), tuple(self.p3))
- return SegmentList(np.array(flatten(points, tolerance)))
-
- def bounds(self, tolerance):
- return self.flattened(tolerance).bounds(tolerance)
\ No newline at end of file
diff --git a/vectorizing/geometry/potrace.py b/vectorizing/geometry/potrace.py
index b3edabc..521cdb7 100644
--- a/vectorizing/geometry/potrace.py
+++ b/vectorizing/geometry/potrace.py
@@ -1,68 +1,41 @@
import numpy as np
+from pathops import Path, FillType
-from vectorizing.geometry.segment_list import SegmentList
-from vectorizing.geometry.cubic_bezier import CubicBezier
-# Given a potrace path, it constructs a compound path
-# using SegmentList and CubicBezier
def potrace_path_to_compound_path(potrace_path):
- compound_path = []
+ """
+ Converts a potrace path into an SKPath.
+ This conversion is needed to later perform boolean operations
+ on compound paths.
- for potrace_curve in potrace_path:
- path = []
+ Parameters:
+ potrace_path: The potrace path
+
+ Returns:
+ The SKPath
+ """
+ compound_path = Path(fillType=FillType.EVEN_ODD)
+ for potrace_curve in potrace_path:
start_x, start_y = potrace_curve.start_point
+ compound_path.moveTo(start_x, start_y)
+
for segment in potrace_curve:
end_x, end_y = segment.end_point
- start = np.array([start_x, start_y])
- end = np.array([end_x, end_y])
if segment.is_corner:
c_x, c_y = segment.c
- c = np.array([c_x, c_y])
- path.append(SegmentList(np.array([start, c, end])))
-
+ compound_path.lineTo(c_x, c_y)
+ compound_path.lineTo(end_x, end_y)
+
else:
c1_x, c1_y = segment.c1
c2_x, c2_y = segment.c2
- p1 = np.array([c1_x, c1_y])
- p2 = np.array([c2_x, c2_y])
- path.append(CubicBezier(start, p1, p2, end))
-
+ compound_path.cubicTo(c1_x, c1_y, c2_x, c2_y, end_x, end_y)
+
start_x = end_x
start_y = end_y
- compound_path.append(path)
-
- return compound_path
-
-# "Unfolds" a folded polygon, meaning:
-# A polygon that can be written in the form p = [...SegmentList, ...SegmentList, ...]
-# In the end, p can be written as p = [...SegmentList]
-def unfold_polygon(folded_polygon):
- unfolded = []
- poly_length = len(folded_polygon)
-
- for idx, segment_list in enumerate(folded_polygon):
- point_list = segment_list.to_list()
- if idx == poly_length - 1:
- unfolded += point_list
- else:
- unfolded += point_list[:len(point_list) - 1]
-
- return unfolded
+ compound_path.close()
-# Given a compound path, it converts it to a compound polygon
-# by flattening curves.
-# All coordinates can be scaled for convenience (see pyclipper)
-def compound_path_to_compound_polygon(compound_path, tolerance, scale=1):
- polygons = [
- [item.flattened(tolerance).scaled(scale) for item in path]
- for path in compound_path
- ]
- return [unfold_polygon(folded_polygon) for folded_polygon in polygons]
-
-# Given a compound polygon, it converts it to a compound path.
-# All coordinates can be scaled for convenience (see pyclipper)
-def compound_polygon_to_compound_path(compound_polygon, scale = 1):
- return [[SegmentList.from_polygon(polygon).scaled(scale)] for polygon in compound_polygon]
\ No newline at end of file
+ return compound_path
diff --git a/vectorizing/geometry/segment_list.py b/vectorizing/geometry/segment_list.py
deleted file mode 100644
index 61192da..0000000
--- a/vectorizing/geometry/segment_list.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import numpy as np
-from vectorizing.geometry.bounds import Bounds
-
-# Class to represent a list of segments
-# IMPORTANT: points should be an ndarray, and not a python list
-class SegmentList:
- def __init__(self, points):
- self.start = points[0]
- self.points = points
-
- def scaled(self, s):
- return SegmentList(self.points * s)
-
- def flattened(self, _):
- return self
-
- def to_list(self):
- return list(self.points)
-
- def bounds(self, _):
- t = self.points.T
- x = t[0]
- y = t[1]
-
- return Bounds(
- np.min(x),
- np.min(y),
- np.max(x),
- np.max(y)
- )
-
- @staticmethod
- def from_polygon(polygon):
- return SegmentList(np.array(polygon))
\ No newline at end of file
diff --git a/vectorizing/solvers/color/ColorSolver.py b/vectorizing/solvers/color/ColorSolver.py
index 9fdb854..fa19490 100644
--- a/vectorizing/solvers/color/ColorSolver.py
+++ b/vectorizing/solvers/color/ColorSolver.py
@@ -6,45 +6,41 @@
from vectorizing.solvers.color.clip import remove_layering
from vectorizing.solvers.color.bitmaps import create_bitmaps
+
class ColorSolver:
- def __init__(self, img, color_count, tolerance, timer):
+ def __init__(self, img, color_count, timer):
color_count = color_count or ColorSolver.DEFAULT_COLOR_COUNT
color_count = max(color_count, ColorSolver.MIN_COLOR_COUNT)
color_count = min(color_count, ColorSolver.MAX_COLOR_COUNT)
self.color_count = color_count
- self.tolerance = tolerance
self.img = limit_size(img)
-
+
# Init image array
self.img_arr = np.asarray(self.img).astype(np.uint8)
self.timer = timer
def solve(self):
- self.timer.start_timer('Quantization')
+ self.timer.start_timer("Quantization")
labels, colors, has_background = quantize(self.img_arr, self.color_count)
self.timer.end_timer()
- self.timer.start_timer('Bitmap Creation')
+ self.timer.start_timer("Bitmap Creation")
bitmaps, colors = create_bitmaps(labels, colors, has_background)
self.timer.end_timer()
- self.timer.start_timer('Bitmap Tracing')
+ self.timer.start_timer("Bitmap Tracing")
traced_bitmaps = [potrace.Bitmap(bitmap).trace() for bitmap in bitmaps]
self.timer.end_timer()
- self.timer.start_timer('Polygon Clipping')
- compound_paths = remove_layering(traced_bitmaps, self.tolerance)
+ self.timer.start_timer("Polygon Clipping")
+ compound_paths = remove_layering(traced_bitmaps)
self.timer.end_timer()
-
- return [
- compound_paths,
- colors,
- self.img.size[0],
- self.img.size[1]
- ]
-
+
+ return [compound_paths, colors, self.img.size[0], self.img.size[1]]
+
+
ColorSolver.MIN_COLOR_COUNT = 2
ColorSolver.DEFAULT_COLOR_COUNT = 6
-ColorSolver.MAX_COLOR_COUNT = 64
\ No newline at end of file
+ColorSolver.MAX_COLOR_COUNT = 64
diff --git a/vectorizing/solvers/color/clip.py b/vectorizing/solvers/color/clip.py
index a409566..e47ce7b 100644
--- a/vectorizing/solvers/color/clip.py
+++ b/vectorizing/solvers/color/clip.py
@@ -1,45 +1,23 @@
-import pyclipper
+from pathops import op, PathOp
+from vectorizing.geometry.potrace import potrace_path_to_compound_path
-from vectorizing.geometry.potrace import (
- potrace_path_to_compound_path,
- compound_path_to_compound_polygon,
- compound_polygon_to_compound_path
-)
+def remove_layering(traced_bitmaps):
+ """
+ Performs boolean operations on a list of traced bitmaps
+ to ensure that they are all disjoint.
-SCALE = 1_000_000
+ Parameters:
+ traced_bitmaps: The list of traced bitmaps (potrace paths).
-# Ensures paths are disjoint after tracing.
-# See bitmaps.py for why this is important.
-
-# NOTE: The clipper library uses integer coordinates only for numerical robustness.
-# That's why coordinates are scaled by a big factor, to preserve precision.
-def remove_layering(traced_bitmaps, tolerance):
+ Returns:
+ The processed list of compound paths.
+ """
compound_paths = [
- potrace_path_to_compound_path(traced)
- for traced in traced_bitmaps
- ]
-
- compound_polygons = [
- compound_path_to_compound_polygon(compound_path, tolerance, SCALE)
- for compound_path in compound_paths
+ potrace_path_to_compound_path(traced) for traced in traced_bitmaps
]
- for x in range(len(compound_polygons) - 1):
- next = compound_polygons[x + 1]
-
- pc = pyclipper.Pyclipper()
- pc.AddPaths(next, pyclipper.PT_CLIP, True)
- pc.AddPaths(compound_polygons[x], pyclipper.PT_SUBJECT, True)
-
- compound_polygons[x] = pc.Execute(
- pyclipper.CT_DIFFERENCE,
- pyclipper.PFT_EVENODD,
- pyclipper.PFT_EVENODD
- )
-
- compound_paths = [
- compound_polygon_to_compound_path(compound_polygon, 1 / SCALE)
- for compound_polygon in compound_polygons
- ]
+ for x in range(len(compound_paths) - 1):
+ next = compound_paths[x + 1]
+ compound_paths[x] = op(compound_paths[x], next, PathOp.DIFFERENCE)
- return compound_paths
\ No newline at end of file
+ return compound_paths
diff --git a/vectorizing/svg/draw.py b/vectorizing/svg/draw.py
deleted file mode 100644
index c89bfe9..0000000
--- a/vectorizing/svg/draw.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from vectorizing.geometry.cubic_bezier import CubicBezier
-from vectorizing.geometry.segment_list import SegmentList
-
-# Truncates a number to 2 decimal places
-def truncate(number):
- return "{:.2f}".format(number)
-
-# Draws a segment list's path data
-def draw_segment_list(segment_list, first_in_path = False):
- l = ' '.join([
- f'L {truncate(point[0])},{truncate(point[1])}'
- for point in segment_list.points[1:]
- ]) + ' '
-
- if first_in_path:
- m = f'M {truncate(segment_list.start[0])},{truncate(segment_list.start[1])} '
- return m + l
-
- return l
-
-# Draws a cubic bezier path data
-def draw_cubic_bezier(cubic_bezier, first_in_path = False):
- c = 'C' \
- f'{truncate(cubic_bezier.p1[0])},{truncate(cubic_bezier.p1[1])} ' \
- f'{truncate(cubic_bezier.p2[0])},{truncate(cubic_bezier.p2[1])} ' \
- f'{truncate(cubic_bezier.p3[0])},{truncate(cubic_bezier.p3[1])} '
-
- if first_in_path:
- m = f'M {truncate(cubic_bezier.p0[0])},{truncate(cubic_bezier.p0[1])} '
- return m + c
-
- return c
-
-class UnknownGeometricEntity (Exception):
- def __init__ (self):
- super().__init__(self, 'draw_geo: Unknown geometric entity.')
-
-def draw_geo(geo, first_in_path = False):
- if isinstance(geo, CubicBezier):
- return draw_cubic_bezier(geo, first_in_path)
- if isinstance(geo, SegmentList):
- return draw_segment_list(geo, first_in_path)
- raise UnknownGeometricEntity()
\ No newline at end of file
diff --git a/vectorizing/svg/markup.py b/vectorizing/svg/markup.py
index ffdf195..e44766a 100644
--- a/vectorizing/svg/markup.py
+++ b/vectorizing/svg/markup.py
@@ -1,26 +1,95 @@
-from vectorizing.svg.draw import draw_geo
+def truncate(number):
+ """
+ Truncates a number to two decimal places.
+ We use this to limit the amount of decimal places used
+ in the final generated SVG markup (reduces file sizes).
-# Converts a color to a valid svg color value
-def to_SVG_color(color):
- func = 'rgb' if len(color) == 3 else 'rgba'
- tuple = ''.join([f'{item},' if i != len(color) - 1 else f'{item}' for i, item in enumerate(color)])
- return f'{func}({tuple})'
+ Parameters:
+ number: The number to truncate.
-# Creates markup of traced paths
-def create_markup(compound_paths, colors, width, height):
+ Returns:
+ The truncated number.
+ """
+ return "{:.2f}".format(number)
+
+
+def to_SVG_color_string(color):
+ """
+ Converts a color (represented as a tuple) to a valid
+ SVG color string.
+
+ Parameters:
+ color: The input color.
+
+ Returns:
+ The SVG color string.
+
+ Example:
+ to_SVG_color_string([20, 30, 40]) => rgb(20, 30, 40)
+ to_SVG_color_string([20, 30, 40, 0.5]) => rgba(20, 30, 40, 0.5)
+
+ """
+ func = "rgb" if len(color) == 3 else "rgba"
+ tuple = "".join(
+ [
+ f"{item}," if i != len(color) - 1 else f"{item}"
+ for i, item in enumerate(color)
+ ]
+ )
+ return f"{func}({tuple})"
+
+
+def generate_SVG_markup(compound_paths, colors, width, height):
+ """
+ Generates SVG markup from a list of compound paths,
+ their respective colors, and the document dimensions.
+
+ Parameters:
+ compound_paths: The compound path list (SKPath).
+ colors: Each compound path's color.
+ width: The width of the document.
+ height: The height of the document.
+
+ Returns:
+ The SVG markup.
+
+ Note:
+ compound paths are assumed to be made out of
+ only lines and cubic beziers.
+ """
paths_markup = []
for compound_path, color in zip(compound_paths, colors):
- if not len(compound_path):
+ segments = list(compound_path.segments)
+ if not len(segments):
continue
- d = ''.join([draw_geo(geo, i == 0) for path in compound_path for i, geo in enumerate(path)]) + 'Z'
- path_markup = f''
+ d = ""
+ for segment in segments:
+ command = segment[0]
+ if command == "moveTo":
+ x = segment[1][0][0]
+ y = segment[1][0][1]
+ d += f"M {truncate(x)} {truncate(y)} "
+ if command == "lineTo":
+ x = segment[1][0][0]
+ y = segment[1][0][1]
+ d += f"L {truncate(x)} {truncate(y)} "
+ if command == "curveTo":
+ d += (
+ "C" + f"{truncate(segment[1][0][0])},{truncate(segment[1][0][1])} "
+ f"{truncate(segment[1][1][0])},{truncate(segment[1][1][1])} "
+ f"{truncate(segment[1][2][0])},{truncate(segment[1][2][1])} "
+ )
+ if command == "closePath":
+ d += "Z"
+
+ path_markup = f''
paths_markup.append(path_markup)
- paths_markup = '\n'.join(paths_markup)
+ paths_markup = "\n".join(paths_markup)
return (
f''
- )
\ No newline at end of file
+ f"\n{paths_markup}\n\n"
+ f""
+ )