From f4c7828049c534693d4a0ce5efdfd68d62954e19 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Sat, 16 Sep 2023 17:24:13 -0500 Subject: [PATCH] Python bindings configured correctly for PyPI releases (#54) * Python bindings sep 2023 (#52) * Added maturin-based Python binding, to be deployed to https://pypi.org/project/vtracer/ * Removed poetry mentions from pyproject.toml, added README_PY.md for use on PYPI * -> v0.6.1 -> moved Python bindings to bottom of converter.rs * - README_PY.md needed to be inside the cmdapp directory to display on PyPi.irg -> v0.6.3 * Move code around * Edit Readme * Edit RELEASES.md * Feature guard * Build wheels with the cmdapp/Cargo.toml rather than top-level Cargo.toml * use cmdapp/Cargo.toml for all Maturin CI actions, which causes Github to build all platforms python wheels and submit a new release to PyPI * Bump to 0.6.4 for new PyPI release with all platforms' wheels included * PyPI didn't accept a 'linux_aarch64' wheel for a release. For the moment, remove the platform until I can convince the action to build 'manylinux_aarch64' or the like * Version bump while I work out CI & PyPI release wrinkles * Maturin authors say `compatibility = "linux"` in pyproject.toml is causing PyPI failure. Replacing with "manylinux2014" * bump to v0.7.0 in preparation for release from original vtracer repo --------- Co-authored-by: Chris Tsang --- .github/workflows/CI.yml | 118 +++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 4 ++ Cargo.toml | 3 +- RELEASES.md | 29 +++++---- cmdapp/Cargo.toml | 12 ++-- cmdapp/pyproject.toml | 28 +++++++++ cmdapp/src/lib.rs | 6 +- cmdapp/src/python.rs | 81 +++++++++++++++++++++++++ cmdapp/vtracer/README.md | 75 +++++++++++++++++++++++ cmdapp/vtracer/__init__.py | 1 + cmdapp/vtracer/vtracer.pyi | 17 ++++++ webapp/Cargo.toml | 4 +- webapp/app/package.json | 2 +- 13 files changed, 356 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/CI.yml create mode 100644 cmdapp/pyproject.toml create mode 100644 cmdapp/src/python.rs create mode 100644 cmdapp/vtracer/README.md create mode 100644 cmdapp/vtracer/__init__.py create mode 100644 cmdapp/vtracer/vtracer.pyi diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..f7c66ed --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,118 @@ +# This file is autogenerated by maturin v1.2.3 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: -m cmdapp/Cargo.toml --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: -m cmdapp/Cargo.toml --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: -m cmdapp/Cargo.toml --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: -m cmdapp/Cargo.toml --out dist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing * + manylinux: auto diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ca4a2..73319ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 0.6.0 - 2023-09-08 + +* Python Binding + ## 0.5.0 - 2022-10-09 * Handle transparent png images (cli) (#23) diff --git a/Cargo.toml b/Cargo.toml index 589c2a6..999f733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,5 @@ members = [ "cmdapp", "webapp", -] \ No newline at end of file +] +resolver = "2" \ No newline at end of file diff --git a/RELEASES.md b/RELEASES.md index bf822b9..fbbe8d6 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,28 +1,27 @@ -Version 0.4.0 (2021-07-23) -========================== +# Version 0.6.0 (2023-09-08) -- SVG path string numeric precision +- Python bindings +# Version 0.5.0 (2022-10-09) -Version 0.3.0 (2021-01-24) -========================== +- Handle transparent png images -- Added cutout mode +# Version 0.4.0 (2021-07-23) +- SVG path string numeric precision -Version 0.2.0 (2020-11-15) -========================== +# Version 0.3.0 (2021-01-24) -- Use relative & closed paths +- Added cutout mode +# Version 0.2.0 (2020-11-15) -Version 0.1.1 (2020-11-01) -========================== +- Use relative & closed paths -- SVG namespace +# Version 0.1.1 (2020-11-01) +- SVG namespace -Version 0.1.0 (2020-10-31) -========================== +# Version 0.1.0 (2020-10-31) -- Initial release \ No newline at end of file +- Initial release diff --git a/cmdapp/Cargo.toml b/cmdapp/Cargo.toml index e2ffa53..8dbf261 100644 --- a/cmdapp/Cargo.toml +++ b/cmdapp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "vtracer" -version = "0.5.0" -authors = ["Chris Tsang "] -edition = "2018" +version = "0.7.0" +authors = ["Chris Tsang "] +edition = "2021" description = "A cmd app to convert images into vector graphics." license = "MIT OR Apache-2.0" homepage = "http://www.visioncortex.org/vtracer" @@ -14,4 +14,8 @@ keywords = ["svg", "computer-graphics"] clap = "2.33.3" image = "0.23.10" visioncortex = { version = "0.8.0" } -fastrand = "1.8" \ No newline at end of file +fastrand = "1.8" +pyo3 = { version = "0.19.0", optional = true } + +[features] +python-binding = ["pyo3"] \ No newline at end of file diff --git a/cmdapp/pyproject.toml b/cmdapp/pyproject.toml new file mode 100644 index 0000000..565248a --- /dev/null +++ b/cmdapp/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "vtracer" +version = "0.7.0" +description = "Python bindings for the Rust Vtracer raster-to-vector library" +authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ] +readme = "vtracer/README.md" +requires-python = ">=3.7" +license = "MIT" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + +[dependencies] +python = "^3.7" + +[dev-dependencies] +maturin = "^1.2" + +[build-system] +requires = ["maturin>=1.2,<2.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["pyo3/extension-module"] +compatibility = "manylinux2014" +sdist-include = ["LICENSE-MIT", "vtracer/README.md"] \ No newline at end of file diff --git a/cmdapp/src/lib.rs b/cmdapp/src/lib.rs index ba5e0bf..3347464 100644 --- a/cmdapp/src/lib.rs +++ b/cmdapp/src/lib.rs @@ -11,7 +11,11 @@ mod config; mod converter; mod svg; +#[cfg(feature = "python-binding")] +mod python; pub use config::*; pub use converter::*; -pub use svg::*; \ No newline at end of file +pub use svg::*; +#[cfg(feature = "python-binding")] +pub use python::*; \ No newline at end of file diff --git a/cmdapp/src/python.rs b/cmdapp/src/python.rs new file mode 100644 index 0000000..65006fe --- /dev/null +++ b/cmdapp/src/python.rs @@ -0,0 +1,81 @@ +use pyo3::prelude::*; + +use visioncortex::{PathSimplifyMode}; +use super::converter::*; + +/// Python binding +#[pyfunction] +fn convert_image_to_svg_py( image_path: &str, + out_path: &str, + colormode: Option<&str>, // "color" or "binary" + hierarchical: Option<&str>, // "stacked" or "cutout" + mode: Option<&str>, // "polygon", "spline", "none" + filter_speckle: Option, // default: 4 + color_precision: Option, // default: 6 + layer_difference: Option, // default: 16 + corner_threshold: Option, // default: 60 + length_threshold: Option, // in [3.5, 10] default: 4.0 + max_iterations: Option, // default: 10 + splice_threshold: Option, // default: 45 + path_precision: Option // default: 8 + ) -> PyResult<()> { + let input_path = PathBuf::from(image_path); + let output_path = PathBuf::from(out_path); + + // TODO: enforce color mode with an enum so that we only + // accept the strings 'color' or 'binary' + let color_mode = match colormode.unwrap_or("color") { + "color" => ColorMode::Color, + "binary" => ColorMode::Binary, + _ => ColorMode::Color, + }; + + let hierarchical = match hierarchical.unwrap_or("stacked") { + "stacked" => Hierarchical::Stacked, + "cutout" => Hierarchical::Cutout, + _ => Hierarchical::Stacked, + }; + + let mode = match mode.unwrap_or("spline") { + "spline" => PathSimplifyMode::Spline, + "polygon" => PathSimplifyMode::Polygon, + "none" => PathSimplifyMode::None, + _ => PathSimplifyMode::Spline, + }; + + let filter_speckle = filter_speckle.unwrap_or(4); + let color_precision = color_precision.unwrap_or(6); + let layer_difference = layer_difference.unwrap_or(16); + let corner_threshold = corner_threshold.unwrap_or(60); + let length_threshold = length_threshold.unwrap_or(4.0); + let splice_threshold = splice_threshold.unwrap_or(45); + let max_iterations = max_iterations.unwrap_or(10); + + let config = Config { + input_path, + output_path, + color_mode, + hierarchical, + filter_speckle, + color_precision, + layer_difference, + mode, + corner_threshold, + length_threshold, + max_iterations, + splice_threshold, + path_precision, + ..Default::default() + }; + + + convert_image_to_svg(config).unwrap(); + Ok(()) +} + +/// A Python module implemented in Rust. +#[pymodule] +fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?; + Ok(()) +} \ No newline at end of file diff --git a/cmdapp/vtracer/README.md b/cmdapp/vtracer/README.md new file mode 100644 index 0000000..78873dd --- /dev/null +++ b/cmdapp/vtracer/README.md @@ -0,0 +1,75 @@ +
+ + + +

VTracer: Python Binding

+ +

+ Raster to Vector Graphics Converter built on top of visioncortex +

+ +

+ Article + | + Demo + | + Download +

+ +Built with 🦀 by The Vision Cortex Research Group + +
+ +## Introduction + +visioncortex VTracer is an open source software to convert raster images (like jpg & png) into vector graphics (svg). It can vectorize graphics and photographs and trace the curves to output compact vector files. + +Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized inputs (Black & White pixmap), VTracer has an image processing pipeline which can handle colored high resolution scans. + +Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrator/using/image-trace.html), VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes. + +VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. At the same time, VTracer can also handle low resolution pixel art, simulating `image-rendering: pixelated` for retro game artworks. + +A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs). + +## Install (Python) + +```shell +pip install vtracer +``` + +### Usage (Python) + +```python +import vtracer + +input_path = "/path/to/some_file.jpg" +output_path = "/path/to/some_file.vtracer.jpg" + +# Minimal example: use all default values, generate a multicolor SVG +vtracer.convert_image_to_svg_py(inp, out) + +# Single-color example. Good for line art, and much faster than full color: +vtracer.convert_image_to_svg_py(inp, out, colormode='binary') + +# All the bells & whistles +vtracer.convert_image_to_svg_py(inp, + out, + colormode = 'color', # ["color"] or "binary" + hierarchical = 'stacked', # ["stacked"] or "cutout" + mode = 'spline', # ["spline"] "polygon", or "none" + filter_speckle = 4, # default: 4 + color_precision = 6, # default: 6 + layer_difference = 16, # default: 16 + corner_threshold = 60, # default: 60 + length_threshold = 4.0, # in [3.5, 10] default: 4.0 + max_iterations = 10, # default: 10 + splice_threshold = 45, # default: 45 + path_precision = 3 # default: 8 + ) + +``` + +## Rust Library + +The (Rust) library can be found on [crates.io/vtracer](//crates.io/crates/vtracer) and [crates.io/vtracer-webapp](//crates.io/crates/vtracer-webapp). diff --git a/cmdapp/vtracer/__init__.py b/cmdapp/vtracer/__init__.py new file mode 100644 index 0000000..3ccb2aa --- /dev/null +++ b/cmdapp/vtracer/__init__.py @@ -0,0 +1 @@ +from .vtracer import convert_image_to_svg_py \ No newline at end of file diff --git a/cmdapp/vtracer/vtracer.pyi b/cmdapp/vtracer/vtracer.pyi new file mode 100644 index 0000000..adc02ac --- /dev/null +++ b/cmdapp/vtracer/vtracer.pyi @@ -0,0 +1,17 @@ +from typing import Optional + +def convert_image_to_svg_py(image_path: str, + out_path: str, + colormode: Optional[str] = None, # ["color"] or "binary" + hierarchical: Optional[str] = None, # ["stacked"] or "cutout" + mode: Optional[str] = None, # ["spline"], "polygon", "none" + filter_speckle: Optional[int] = None, # default: 4 + color_precision: Optional[int] = None, # default: 6 + layer_difference: Optional[int] = None, # default: 16 + corner_threshold: Optional[int] = None, # default: 60 + length_threshold: Optional[float] = None, # in [3.5, 10] default: 4.0 + max_iterations: Optional[int] = None, # default: 10 + splice_threshold: Optional[int] = None, # default: 45 + path_precision: Optional[int] = None, # default: 8 + ) -> None: + ... diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 6b2eec0..d06f8cd 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "vtracer-webapp" version = "0.4.0" -authors = ["Chris Tsang "] -edition = "2018" +authors = ["Chris Tsang "] +edition = "2021" description = "A web app to convert images into vector graphics." license = "MIT OR Apache-2.0" homepage = "http://www.visioncortex.org/vtracer" diff --git a/webapp/app/package.json b/webapp/app/package.json index 50717ea..438f90a 100644 --- a/webapp/app/package.json +++ b/webapp/app/package.json @@ -2,7 +2,7 @@ "name": "vtracer-app", "version": "0.1.0", "description": "VTracer Webapp", - "author": "Chris Tsang ", + "author": "Chris Tsang ", "license": "proprietary", "private": true, "main": "index.js",