From e84a86d4ac0caf29d6074728376ff0a594243fec Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Thu, 4 Mar 2021 18:40:53 +0100 Subject: [PATCH] Update for Inkscape 1.0 (#880) * update for inkscape 1.0 * add about extension * Build improvements for the inkscape1.0 branch (#985) * zip: export real svg not stitch plan * #411 and #726 * Tools for Font Creators (#1018) * ignore very small holes in fills * remove embroider (#1026) * auto_fill: ignore shrink_or_grow if result is empty (#589) * break apart: do not ignore small fills Co-authored-by: Hagen Fritsch Co-authored-by: Lex Neva --- .github/workflows/build.yml | 129 +++++++++--------- .gitignore | 1 + Makefile | 8 +- bin/build-distribution-archives | 10 +- bin/build-python | 5 - bin/generate-version-file | 10 ++ bin/inkstitch-fonts-gettext | 8 +- bin/pyembroidery-gettext | 4 +- .../.electron-vue/webpack.renderer.config.js | 18 ++- electron/.electron-vue/webpack.web.config.js | 18 ++- electron/src/lib/i18n.js | 28 +++- electron/src/main/index.js | 2 - electron/src/renderer/assets/js/simulator.js | 2 +- .../src/renderer/assets/style/simulator.css | 28 +++- .../src/renderer/components/Simulator.vue | 6 +- electron/src/renderer/main.js | 2 +- icons/inkstitch_colour_logo.svg | 27 ++++ inkstitch.py | 30 ++-- lib/api/install.py | 2 +- lib/api/server.py | 9 +- lib/api/stitch_plan.py | 4 +- lib/commands.py | 88 ++++++------ lib/debug.py | 37 ++--- lib/elements/__init__.py | 22 +-- lib/elements/auto_fill.py | 28 +++- lib/elements/clone.py | 73 ++-------- lib/elements/element.py | 56 ++++---- lib/elements/fill.py | 6 + lib/elements/image.py | 14 +- lib/elements/polyline.py | 7 +- lib/elements/satin_column.py | 47 ++++--- lib/elements/stroke.py | 10 +- lib/elements/svg_objects.py | 71 ---------- lib/elements/text.py | 14 +- lib/extensions/__init__.py | 57 ++++---- lib/extensions/auto_satin.py | 4 +- lib/extensions/base.py | 45 ++---- lib/extensions/break_apart.py | 12 +- lib/extensions/cleanup.py | 19 ++- lib/extensions/commands.py | 4 +- lib/extensions/convert_to_satin.py | 35 +++-- lib/extensions/cut_satin.py | 6 +- lib/extensions/embroider.py | 87 ------------ lib/extensions/embroider_settings.py | 17 +++ lib/extensions/flip.py | 9 +- lib/extensions/import_threadlist.py | 19 +-- lib/extensions/input.py | 11 +- lib/extensions/layer_commands.py | 30 ++-- lib/extensions/lettering.py | 82 ++++++----- lib/extensions/lettering_custom_font_dir.py | 48 +++++++ lib/extensions/lettering_generate_json.py | 76 +++++++++++ lib/extensions/lettering_remove_kerning.py | 30 ++++ lib/extensions/object_commands.py | 2 +- lib/extensions/output.py | 10 +- lib/extensions/params.py | 38 +++--- lib/extensions/print_pdf.py | 35 +++-- lib/extensions/remove_embroidery_settings.py | 20 +-- lib/extensions/reorder.py | 36 +++++ lib/extensions/stitch_plan_preview.py | 9 +- lib/extensions/troubleshoot.py | 27 ++-- lib/extensions/zip.py | 34 ++--- lib/gui/electron.py | 4 +- lib/gui/presets.py | 4 +- lib/gui/simulator.py | 26 ++-- lib/i18n.py | 4 +- lib/inx/__init__.py | 2 +- lib/inx/about.py | 7 + lib/inx/extensions.py | 11 +- lib/inx/generate.py | 4 +- lib/inx/info.py | 9 ++ lib/inx/inputs.py | 2 +- lib/inx/outputs.py | 13 +- lib/inx/utils.py | 31 +++-- lib/lettering/__init__.py | 2 +- lib/lettering/font.py | 44 +++--- lib/lettering/font_variant.py | 22 ++- lib/lettering/glyph.py | 31 +++-- lib/lettering/kerning.py | 69 ++++++++++ lib/output.py | 7 +- lib/stitch_plan/stitch_plan.py | 4 +- lib/stitches/__init__.py | 6 +- lib/stitches/auto_fill.py | 15 +- lib/stitches/auto_satin.py | 29 ++-- lib/stitches/fill.py | 5 +- lib/stitches/running_stitch.py | 2 +- lib/svg/guides.py | 8 +- lib/svg/path.py | 29 ++-- lib/svg/rendering.py | 59 ++++---- lib/svg/svg.py | 3 +- lib/svg/tags.py | 5 +- lib/svg/units.py | 25 +++- lib/threads/__init__.py | 6 +- lib/threads/catalog.py | 2 +- lib/threads/color.py | 9 +- lib/threads/palette.py | 5 +- lib/utils/dotdict.py | 2 +- lib/utils/geometry.py | 6 +- lib/utils/inkscape.py | 4 +- lib/utils/io.py | 22 +-- lib/utils/version.py | 17 +++ print/resources/inkstitch.js | 4 +- print/resources/style.css | 3 +- print/templates/operator_detailedview.html | 2 +- pyembroidery | 2 +- requirements.txt | 14 +- templates/about.xml | 28 ++++ templates/{auto_satin.inx => auto_satin.xml} | 0 .../{break_apart.inx => break_apart.xml} | 0 templates/{cleanup.inx => cleanup.xml} | 0 ...vert_to_satin.inx => convert_to_satin.xml} | 0 templates/{cut_satin.inx => cut_satin.xml} | 0 templates/embroider.inx | 28 ---- templates/embroider.xml | 18 +++ templates/embroider_settings.xml | 21 +++ templates/{flip.inx => flip.xml} | 0 ...lobal_commands.inx => global_commands.xml} | 0 ...t_threadlist.inx => import_threadlist.xml} | 4 +- templates/{input.inx => input.xml} | 0 templates/{install.inx => install.xml} | 2 +- ...{layer_commands.inx => layer_commands.xml} | 0 templates/{lettering.inx => lettering.xml} | 2 +- templates/lettering_custom_font_dir.xml | 26 ++++ templates/lettering_generate_json.xml | 46 +++++++ templates/lettering_remove_kerning.xml | 28 ++++ ...bject_commands.inx => object_commands.xml} | 0 templates/{output.inx => output.xml} | 2 +- templates/output_params_txt.xml | 2 +- templates/{params.inx => params.xml} | 2 +- templates/{print.inx => print.xml} | 2 +- ...ngs.inx => remove_embroidery_settings.xml} | 0 templates/reorder.xml | 15 ++ templates/{simulator.inx => simulator.xml} | 2 +- ...an_preview.inx => stitch_plan_preview.xml} | 0 .../{troubleshoot.inx => troubleshoot.xml} | 0 templates/{zip.inx => zip.xml} | 4 +- 135 files changed, 1435 insertions(+), 1003 deletions(-) create mode 100755 bin/generate-version-file create mode 100644 icons/inkstitch_colour_logo.svg delete mode 100644 lib/elements/svg_objects.py delete mode 100644 lib/extensions/embroider.py create mode 100644 lib/extensions/embroider_settings.py create mode 100644 lib/extensions/lettering_custom_font_dir.py create mode 100644 lib/extensions/lettering_generate_json.py create mode 100644 lib/extensions/lettering_remove_kerning.py create mode 100644 lib/extensions/reorder.py create mode 100755 lib/inx/about.py create mode 100755 lib/inx/info.py create mode 100644 lib/lettering/kerning.py create mode 100644 lib/utils/version.py create mode 100644 templates/about.xml rename templates/{auto_satin.inx => auto_satin.xml} (100%) rename templates/{break_apart.inx => break_apart.xml} (100%) rename templates/{cleanup.inx => cleanup.xml} (100%) rename templates/{convert_to_satin.inx => convert_to_satin.xml} (100%) rename templates/{cut_satin.inx => cut_satin.xml} (100%) delete mode 100644 templates/embroider.inx create mode 100644 templates/embroider.xml create mode 100644 templates/embroider_settings.xml rename templates/{flip.inx => flip.xml} (100%) rename templates/{global_commands.inx => global_commands.xml} (100%) rename templates/{import_threadlist.inx => import_threadlist.xml} (75%) rename templates/{input.inx => input.xml} (100%) rename templates/{install.inx => install.xml} (92%) rename templates/{layer_commands.inx => layer_commands.xml} (100%) rename templates/{lettering.inx => lettering.xml} (92%) create mode 100644 templates/lettering_custom_font_dir.xml create mode 100644 templates/lettering_generate_json.xml create mode 100644 templates/lettering_remove_kerning.xml rename templates/{object_commands.inx => object_commands.xml} (100%) rename templates/{output.inx => output.xml} (93%) rename templates/{params.inx => params.xml} (92%) rename templates/{print.inx => print.xml} (93%) rename templates/{remove_embroidery_settings.inx => remove_embroidery_settings.xml} (100%) create mode 100644 templates/reorder.xml rename templates/{simulator.inx => simulator.xml} (93%) rename templates/{stitch_plan_preview.inx => stitch_plan_preview.xml} (100%) rename templates/{troubleshoot.inx => troubleshoot.xml} (100%) rename templates/{zip.inx => zip.xml} (89%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76f221a99..0da74dee2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,156 +10,149 @@ jobs: linux: runs-on: ubuntu-16.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: submodules: recursive + - uses: actions/setup-python@v2 + with: + python-version: '3.9' - uses: actions/setup-node@v1 with: - node-version: '11.x' - - name: download dependencies - shell: bash - run: | - curl -sOL https://inkscape.org/en/gallery/item/12187/inkscape-0.92.3.tar.bz2 + node-version: '15.x' + + - uses: actions/cache@v2 + id: pip-cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-16.04-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-16.04-pip- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: install dependencies shell: bash run: | - # I'd love to use a setup-python action but it seems to give a - # python that doesn't support unicode. See: - # https://github.com/actions/setup-python/issues/23 - sudo apt-get update - - sudo apt-get install python2.7 python -m pip install --upgrade pip + python -m pip install wheel sudo apt-get install gettext # for wxPython - sudo apt-get install glib-networking libsdl1.2-dev + sudo apt install glib-networking libsdl1.2-dev # for PyGObject - sudo apt install libgirepository1.0-dev + sudo apt install libgirepository1.0-dev libcairo2-dev # for shapely - sudo apt install libgeos-dev + sudo apt install libgeos-dev build-essential libgtk-3-dev uname -a python --version python -m pip --version python -m pip debug - # wxPython doen't publish linux wheels in pypi - wget -q https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04/wxPython-4.0.6-cp27-cp27mu-linux_x86_64.whl - python -m pip install wxPython*.whl + python -m pip install pycairo==1.11.1 + python -m pip install PyGObject==3.30.5 - python -m pip install PyGObject + # colormath - last official release: 3.0.0 + # we need already submitted fixes - so let's grab them from the github repository + python -m pip install git+https://github.com/gtaylor/python-colormath python -m pip install -r requirements.txt - python -m pip install pyinstaller==3.3.1 + python -m pip install pyinstaller - tar -jxf inkscape-0.92.3.tar.bz2 - rm inkscape-0.92.3.tar.bz2 - mv inkscape-0.92.3 inkscape - echo "${{ env.pythonLocation }}\bin" >> $GITHUB_PATH - shell: bash run: | make dist env: BUILD: linux - - uses: actions/upload-artifact@master + - uses: actions/upload-artifact@v2 with: name: inkstitch-linux path: artifacts windows: runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: submodules: recursive - uses: actions/setup-node@v1 with: - node-version: '11.x' - - uses: actions/setup-python@v1 + node-version: '15.x' + - uses: actions/setup-python@v2 with: - python-version: '2.7.x' + python-version: '3.9' architecture: 'x86' - uses: microsoft/setup-msbuild@v1.0.2 - - name: download dependencies - shell: bash - run: | - curl -sOL https://github.com/lexelby/inkstitch-build-objects/releases/download/v1.0.0/Shapely-1.6.3-cp27-cp27m-win32.whl - curl -sOL https://inkscape.org/en/gallery/item/12187/inkscape-0.92.3.tar.bz2 - name: install dependencies shell: bash run: | - pip install Shapely-1.6.3-cp27-cp27m-win32.whl - pip install -r requirements.txt - pip install pyinstaller==3.3.1 + python -m pip install --upgrade pip + python -m pip install wheel - # Just using tar -j freezes forever with no output. Heck if I know why. This seems to work. - bzcat inkscape-0.92.3.tar.bz2 | tar -vxf - - rm inkscape-0.92.3.tar.bz2 - mv inkscape-0.92.3 inkscape + python -m pip install git+https://github.com/gtaylor/python-colormath + + python -m pip install -r requirements.txt + python -m pip install pyinstaller echo "${{ env.pythonLocation }}\bin" >> $GITHUB_PATH - - name: fix geos - shell: bash - run: | - cd "${{ env.pythonLocation }}\Lib/site-packages/shapely/DLLs" - cp geos_c.dll geos.dll - shell: bash run: | make dist env: BUILD: windows - - uses: actions/upload-artifact@master + - uses: actions/upload-artifact@v2 with: name: inkstitch-windows path: artifacts mac: runs-on: macos-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: submodules: recursive + - uses: actions/setup-python@v2 + with: + python-version: '3.9' - uses: actions/setup-node@v1 with: - node-version: '11.x' - - uses: actions/setup-python@v1 - with: - python-version: '2.7.x' - - uses: actions/setup-node@v1 - with: - node-version: '10.x' - - name: download inkscape - shell: bash - run: | - curl -sOL https://inkscape.org/en/gallery/item/12187/inkscape-0.92.3.tar.bz2 + node-version: '15.x' - name: install dependencies shell: bash run: | brew update - # this errors because it installs python3 but python2 is already installed - brew install gtk+3 pkg-config gobject-introspection libffi gettext || true + brew install gtk+3 pkg-config gobject-introspection geos libffi gettext || true export LDFLAGS="-L/usr/local/opt/libffi/lib" export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig" - + # for msgfmt echo "/usr/local/opt/gettext/bin" >> $GITHUB_PATH echo "GI_TYPELIB_PATH=/usr/local/lib/girepository-1.0/" >> $GITHUB_ENV + pip install --upgrade pip pip --version + pip install wheel pip install PyGObject + pip install git+https://github.com/gtaylor/python-colormath + pip install -r requirements.txt - pip install pyinstaller==3.3.1 - - tar -jxf inkscape-0.92.3.tar.bz2 - rm inkscape-0.92.3.tar.bz2 - mv inkscape-0.92.3 inkscape + pip install pyinstaller echo "${{ env.pythonLocation }}/bin" >> $GITHUB_PATH - shell: bash @@ -167,7 +160,7 @@ jobs: make dist env: BUILD: osx - - uses: actions/upload-artifact@master + - uses: actions/upload-artifact@v2 with: name: inkstitch-mac path: artifacts diff --git a/.gitignore b/.gitignore index 4ff89f902..09f99873b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ locales/ /debug.log /debug.svg /.idea +/VERSION diff --git a/Makefile b/Makefile index b5bbd4e58..3253a570f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -dist: locales inx +dist: version locales inx bash bin/build-python bash bin/build-electron bash bin/build-distribution-archives @@ -8,7 +8,7 @@ distclean: rm -rf build dist inx locales *.spec *.tar.gz *.zip electron/node_modules electron/dist .PHONY: inx -inx: locales +inx: version locales mkdir -p inx python bin/generate-inx-files; \ @@ -49,6 +49,10 @@ locales: mkdir -p locales; \ fi +.PHONY: version +version: + bash bin/generate-version-file + .PHONY: style style: flake8 . --count --max-complexity=10 --max-line-length=150 --statistics --exclude=pyembroidery,__init__.py,electron,build diff --git a/bin/build-distribution-archives b/bin/build-distribution-archives index 6e11818d2..6402122b8 100755 --- a/bin/build-distribution-archives +++ b/bin/build-distribution-archives @@ -4,12 +4,14 @@ VERSION="$(echo ${GITHUB_REF} | sed -e 's|refs/heads/||' -e 's|refs/tags/||' -e OS="${BUILD:-$(uname)}" ARCH="$(uname -m)" -cp -a images/examples palettes symbols fonts dist/inkstitch -cp -a icons locales print dist/inkstitch/bin - if [ "$BUILD" = "osx" ]; then - cp -a electron/build/mac dist/inkstitch/electron + cp -a images/examples palettes symbols fonts LICENSE VERSION dist/inkstitch.app/Contents + cp -a icons locales print dist/inkstitch.app/Contents/MacOS + cp -a electron/build/mac dist/inkstitch.app/Contents/electron + rm -rf dist/inkstitch/ else + cp -a images/examples palettes symbols fonts LICENSE VERSION dist/inkstitch + cp -a icons locales print dist/inkstitch/bin cp -a electron/build/*-unpacked dist/inkstitch/electron fi diff --git a/bin/build-python b/bin/build-python index 0dd3e0a03..88e166aea 100755 --- a/bin/build-python +++ b/bin/build-python @@ -52,8 +52,3 @@ shopt -s dotglob mkdir dist/bin mv dist/inkstitch/* dist/bin mv dist/bin dist/inkstitch - -# on Mac, pyinstaller creates a .app version as well, but we don't need that -if [ "$BUILD" = "osx" ]; then - rm -rf dist/inkstitch.app/ -fi diff --git a/bin/generate-version-file b/bin/generate-version-file new file mode 100755 index 000000000..36c70944c --- /dev/null +++ b/bin/generate-version-file @@ -0,0 +1,10 @@ +#!/bin/bash + +VERSION="${GITHUB_REF##*/}" +OS="${BUILD:-$(uname)}" + +if [[ "$VERSION" == "" ]]; then + VERSION="Manual Install" +fi + +echo "${VERSION} (${OS})" > VERSION diff --git a/bin/inkstitch-fonts-gettext b/bin/inkstitch-fonts-gettext index 8b802fee3..24fb9fdf5 100755 --- a/bin/inkstitch-fonts-gettext +++ b/bin/inkstitch-fonts-gettext @@ -12,8 +12,8 @@ for font in sorted(os.listdir(fonts_dir)): with open(os.path.join(fonts_dir, font, "font.json")) as font_json: font_metadata = json.load(font_json) - print "# L10N name of font in fonts/%s" % font - print "_(%s)" % repr(font_metadata.get("name", "")) + print("# L10N name of font in fonts/%s" % font) + print("_(%s)" % repr(font_metadata.get("name", ""))) - print "# L10N description of font in fonts/%s" % font - print "_(%s)" % repr(font_metadata.get("description", "")) + print("# L10N description of font in fonts/%s" % font) + print("_(%s)" % repr(font_metadata.get("description", ""))) diff --git a/bin/pyembroidery-gettext b/bin/pyembroidery-gettext index ac9bd1ab5..f4e844b95 100755 --- a/bin/pyembroidery-gettext +++ b/bin/pyembroidery-gettext @@ -6,5 +6,5 @@ import pyembroidery # generate fake python code containing the descriptions of pyembroidery formats # as gettext calls so that pybabel will extract them into messages.po for format in pyembroidery.supported_formats(): - print "# L10N description for pyembroidery file format: %s" % format['extension'] - print "_(%s)" % repr(format['description']) + print("# L10N description for pyembroidery file format: %s" % format['extension']) + print("_(%s)" % repr(format['description'])) diff --git a/electron/.electron-vue/webpack.renderer.config.js b/electron/.electron-vue/webpack.renderer.config.js index 89f8cd783..ac281db65 100644 --- a/electron/.electron-vue/webpack.renderer.config.js +++ b/electron/.electron-vue/webpack.renderer.config.js @@ -103,12 +103,6 @@ let rendererConfig = { 'css-loader', { loader: 'sass-loader', - // Requires sass-loader@^7.0.0 - options: { - implementation: require('sass'), - fiber: require('fibers'), - indentedSyntax: true // optional - }, // Requires sass-loader@^8.0.0 options: { implementation: require('sass'), @@ -132,6 +126,18 @@ let rendererConfig = { new HtmlWebpackPlugin({ filename: 'index.html', template: path.resolve(__dirname, '../src/index.ejs'), + templateParameters(compilation, assets, options) { + return { + compilation: compilation, + webpack: compilation.getStats().toJson(), + webpackConfig: compilation.options, + htmlWebpackPlugin: { + files: assets, + options: options + }, + process + } + }, minify: { collapseWhitespace: true, removeAttributeQuotes: true, diff --git a/electron/.electron-vue/webpack.web.config.js b/electron/.electron-vue/webpack.web.config.js index 913518a25..50a73df6e 100644 --- a/electron/.electron-vue/webpack.web.config.js +++ b/electron/.electron-vue/webpack.web.config.js @@ -77,12 +77,6 @@ let webConfig = { 'css-loader', { loader: 'sass-loader', - // Requires sass-loader@^7.0.0 - options: { - implementation: require('sass'), - fiber: require('fibers'), - indentedSyntax: true // optional - }, // Requires sass-loader@^8.0.0 options: { implementation: require('sass'), @@ -102,6 +96,18 @@ let webConfig = { new HtmlWebpackPlugin({ filename: 'index.html', template: path.resolve(__dirname, '../src/index.ejs'), + templateParameters(compilation, assets, options) { + return { + compilation: compilation, + webpack: compilation.getStats().toJson(), + webpackConfig: compilation.options, + htmlWebpackPlugin: { + files: assets, + options: options + }, + process + } + }, minify: { collapseWhitespace: true, removeAttributeQuotes: true, diff --git a/electron/src/lib/i18n.js b/electron/src/lib/i18n.js index 886fd654e..4f9395d71 100644 --- a/electron/src/lib/i18n.js +++ b/electron/src/lib/i18n.js @@ -1,9 +1,27 @@ -module.exports.selectLanguage = function () { - ['LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'].forEach(language => { +module.exports.selectLanguage = function (translations) { + // get a list of available translations + var availableTranslations = ['en_US']; + for(var k in translations) availableTranslations.push(k); + + var lang = undefined; + + // get system language / Inkscape language + ['LANG', 'LC_MESSAGES', 'LC_ALL', 'LANGUAGE'].forEach(language => { if (process.env[language]) { - return process.env[language].split(":")[0] + // split encoding information, we don't need it + var current_lang = process.env[language].split(".")[0]; + if (current_lang.length == 2) { + // current language has only two letters (e.g. en), + // compare with available languages and if present, set to a long locale name (e.g. en_US) + lang = availableTranslations.find(elem => elem.startsWith(current_lang)); + } else { + lang = current_lang; + } } }) - - return "en_US" + // set default language + if (lang === undefined) { + lang = "en_US" + } + return lang } diff --git a/electron/src/main/index.js b/electron/src/main/index.js index f7fb9437c..14a3da55e 100644 --- a/electron/src/main/index.js +++ b/electron/src/main/index.js @@ -61,9 +61,7 @@ function createWindow() { app.on('ready', createWindow) app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { app.quit() - } }) app.on('activate', () => { diff --git a/electron/src/renderer/assets/js/simulator.js b/electron/src/renderer/assets/js/simulator.js index 638f0ca23..6c251a59b 100644 --- a/electron/src/renderer/assets/js/simulator.js +++ b/electron/src/renderer/assets/js/simulator.js @@ -126,7 +126,7 @@ export default { return "" } - let label = "STITCH" + let label = this.$gettext("STITCH") switch (true) { case stitch.jump: label = this.$gettext("JUMP") diff --git a/electron/src/renderer/assets/style/simulator.css b/electron/src/renderer/assets/style/simulator.css index 7da1d9ef4..30e601b08 100644 --- a/electron/src/renderer/assets/style/simulator.css +++ b/electron/src/renderer/assets/style/simulator.css @@ -20,7 +20,18 @@ } button { - color: rgb(0, 51, 153) + color: rgb(0, 51, 153); + align-items: flex-start; + text-align: center; + cursor: default; + background-color: buttonface; + box-sizing: border-box; + padding: 2px 6px 3px; + border-width: 2px; + border-style: outset; + border-color: buttonface; + border-image: initial; + margin-bottom: 5px; } .fa-spin-fast { @@ -59,11 +70,13 @@ button { text-align: center; flex: 1; white-space: nowrap; + margin: 0 5px; } fieldset { border: 2px solid rgb(0, 51, 153); position: relative; + padding: 0 5px } .window-controls { @@ -142,6 +155,10 @@ fieldset.show-commands { text-align: left; } +fieldset.show-commands legend { + text-align: center; +} + fieldset.show-commands span { display: inline-block; vertical-align: top; @@ -152,7 +169,7 @@ fieldset.show-commands span.npp { } fieldset.show-commands span:first-of-type { - padding-right: 12px; + padding: 0 5px; } button.pressed { @@ -164,8 +181,7 @@ button.pressed { } .slider-container { - margin-top: 25px; - margin-bottom: 25px; + margin: 25px 5px; flex-grow: 0; } @@ -244,12 +260,15 @@ button.pressed { width: 4rem; float: right; font-size: 1rem; + border-style: inset; + padding: 0 3px; } .simulator { display: flex; flex-direction: column; height: 95vh; + margin: 10px; } .current-command { @@ -269,6 +288,7 @@ div.simulator::v-deep svg.simulation { div.simulator::v-deep svg.simulation-scale { height: 50px; order: -1; + margin-left: 5px; } div.simulator::v-deep .simulation-scale-label { diff --git a/electron/src/renderer/components/Simulator.vue b/electron/src/renderer/components/Simulator.vue index c6a190fbf..66abadd60 100644 --- a/electron/src/renderer/components/Simulator.vue +++ b/electron/src/renderer/components/Simulator.vue @@ -189,7 +189,9 @@ {{currentCommand}}
- Show + + Show + @@ -211,7 +213,7 @@ - needle
points
+ needle points
diff --git a/electron/src/renderer/main.js b/electron/src/renderer/main.js index 25a3f7f12..ee7dba027 100644 --- a/electron/src/renderer/main.js +++ b/electron/src/renderer/main.js @@ -73,7 +73,7 @@ Vue.component('font-awesome-layers', FontAwesomeLayers) Vue.use(Transitions) Vue.use(GetTextPlugin, { translations: translations, - defaultLanguage: selectLanguage(), + defaultLanguage: selectLanguage(translations), silent: true }) diff --git a/icons/inkstitch_colour_logo.svg b/icons/inkstitch_colour_logo.svg new file mode 100644 index 000000000..c5214ec04 --- /dev/null +++ b/icons/inkstitch_colour_logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/inkstitch.py b/inkstitch.py index 58d0f434f..13eab8098 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -3,12 +3,15 @@ import os import sys import traceback from argparse import ArgumentParser -from cStringIO import StringIO +from io import StringIO + +from inkex import errormsg +from lxml.etree import XMLSyntaxError import lib.debug as debug from lib import extensions from lib.i18n import _ -from lib.utils import restore_stderr, save_stderr +from lib.utils import restore_stderr, save_stderr, version logger = logging.getLogger('shapely.geos') logger.setLevel(logging.DEBUG) @@ -36,28 +39,35 @@ extension_class = getattr(extensions, extension_class_name) extension = extension_class() if hasattr(sys, 'gettrace') and sys.gettrace(): - extension.affect(args=remaining_args) + extension.run(args=remaining_args) else: save_stderr() exception = None try: - extension.affect(args=remaining_args) + extension.run(args=remaining_args) except (SystemExit, KeyboardInterrupt): raise + except XMLSyntaxError: + msg = _("Ink/Stitch cannot read your SVG file. " + "This is often the case when you use a file which has been created with Adobe Illustrator.") + msg += "\n\n" + msg += _("Try to import the file into Inkscape through 'File > Import...' (Ctrl+I)") + errormsg(msg) except Exception: exception = traceback.format_exc() finally: restore_stderr() if shapely_errors.tell(): - print >> sys.stderr, shapely_errors.getvalue() + errormsg(shapely_errors.getvalue()) if exception: - print >> sys.stderr, _("Ink/Stitch experienced an unexpected error.").encode("UTF-8") - print >> sys.stderr, _("If you'd like to help, please file an issue at " - "https://github.com/inkstitch/inkstitch/issues " - "and include the entire error description below:").encode("UTF-8"), "\n" - print >> sys.stderr, exception + errormsg(_("Ink/Stitch experienced an unexpected error.") + "\n") + errormsg(_("If you'd like to help, please file an issue at " + "https://github.com/inkstitch/inkstitch/issues " + "and include the entire error description below:") + "\n") + errormsg(version.get_inkstitch_version() + "\n") + errormsg(exception) sys.exit(1) else: sys.exit(0) diff --git a/lib/api/install.py b/lib/api/install.py index 20138973a..f52233fbe 100644 --- a/lib/api/install.py +++ b/lib/api/install.py @@ -16,7 +16,7 @@ def palettes(): path = os.path.join(base_path, 'palettes') src_dir = get_bundled_dir('palettes') copy_files(glob(os.path.join(src_dir, "*")), path) - except Exception, exc: + except Exception as exc: return jsonify({"error": str(exc)}), 500 return jsonify({"status": "success"}) diff --git a/lib/api/server.py b/lib/api/server.py index bdfa45731..0db253c67 100644 --- a/lib/api/server.py +++ b/lib/api/server.py @@ -1,16 +1,17 @@ import errno import logging import socket +import sys import time from threading import Thread import requests from flask import Flask, g, request +from ..utils.json import InkStitchJSONEncoder from .install import install from .simulator import simulator from .stitch_plan import stitch_plan -from ..utils.json import InkStitchJSONEncoder class APIServer(Thread): @@ -27,6 +28,10 @@ class APIServer(Thread): self.__setup_app() def __setup_app(self): # noqa: C901 + # Disable warning about using a development server in a production environment + cli = sys.modules['flask.cli'] + cli.show_server_banner = lambda *x: None + self.app = Flask(__name__) self.app.json_encoder = InkStitchJSONEncoder @@ -89,7 +94,7 @@ class APIServer(Thread): response = requests.get("http://%s:%s/ping" % (self.host, self.port)) if response.status_code == 200: break - except socket.error, e: + except socket.error as e: if e.errno == errno.ECONNREFUSED: pass else: diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py index fd6bf9c95..95cdc7d85 100644 --- a/lib/api/stitch_plan.py +++ b/lib/api/stitch_plan.py @@ -11,7 +11,9 @@ def get_stitch_plan(): if not g.extension.get_elements(): return dict(colors=[], stitch_blocks=[], commands=[]) + metadata = g.extension.get_inkstitch_metadata() + collapse_len = metadata['collapse_len_mm'] patches = g.extension.elements_to_patches(g.extension.elements) - stitch_plan = patches_to_stitch_plan(patches) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) return jsonify(stitch_plan) diff --git a/lib/commands.py b/lib/commands.py index 9d0b243c2..ba3074878 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -3,11 +3,9 @@ import sys from copy import deepcopy from random import random -from shapely import geometry as shgeo - -import cubicsuperpath import inkex -import simpletransform +from lxml import etree +from shapely import geometry as shgeo from .i18n import N_, _ from .svg import (apply_transforms, generate_unique_id, @@ -104,7 +102,7 @@ class Command(BaseCommand): self.parse_command() def parse_connector_path(self): - path = cubicsuperpath.parsePath(self.connector.get('d')) + path = inkex.paths.Path(self.connector.get('d')).to_superpath() return apply_transforms(path, self.connector) def parse_command(self): @@ -153,7 +151,7 @@ class StandaloneCommand(BaseCommand): def point(self): pos = [float(self.node.get("x", 0)), float(self.node.get("y", 0))] transform = get_node_transform(self.node) - simpletransform.applyTransformToPoint(transform, pos) + pos = inkex.transforms.Transform(transform).apply_to_point(pos) return Point(*pos) @@ -209,14 +207,14 @@ def global_command(svg, command): if len(commands) == 1: return commands[0] elif len(commands) > 1: - print >> sys.stderr, _("Error: there is more than one %(command)s command in the document, but there can only be one. " - "Please remove all but one.") % dict(command=command) + print(_("Error: there is more than one %(command)s command in the document, but there can only be one. " + "Please remove all but one.") % dict(command=command), file=sys.stderr) # L10N This is a continuation of the previous error message, letting the user know # what command we're talking about since we don't normally expose the actual # command name to them. Contents of %(description)s are in a separate translation # string. - print >> sys.stderr, _("%(command)s: %(description)s") % dict(command=command, description=_(get_command_description(command))) + print(_("%(command)s: %(description)s") % dict(command=command, description=_(get_command_description(command))), file=sys.stderr) sys.exit(1) else: @@ -256,7 +254,7 @@ def symbols_path(): @cache def symbols_svg(): with open(symbols_path()) as symbols_file: - return inkex.etree.parse(symbols_file) + return etree.parse(symbols_file) @cache @@ -269,7 +267,7 @@ def get_defs(document): defs = document.find(SVG_DEFS_TAG) if defs is None: - defs = inkex.etree.SubElement(document, SVG_DEFS_TAG) + defs = etree.SubElement(document, SVG_DEFS_TAG) return defs @@ -284,7 +282,7 @@ def ensure_symbol(document, command): def add_group(document, node, command): - return inkex.etree.SubElement( + return etree.SubElement( node.getparent(), SVG_GROUP_TAG, { @@ -304,35 +302,35 @@ def add_connector(document, symbol, element): if element.node.get('id') is None: element.node.set('id', generate_unique_id(document, "object")) - path = inkex.etree.Element(SVG_PATH_TAG, - { - "id": generate_unique_id(document, "command_connector"), - "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", - CONNECTION_START: "#%s" % symbol.get('id'), - CONNECTION_END: "#%s" % element.node.get('id'), - CONNECTOR_TYPE: "polyline", + path = etree.Element(SVG_PATH_TAG, + { + "id": generate_unique_id(document, "command_connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", - # l10n: the name of the line that connects a command to the object it applies to - INKSCAPE_LABEL: _("connector") - }) + # l10n: the name of the line that connects a command to the object it applies to + INKSCAPE_LABEL: _("connector") + }) symbol.getparent().insert(0, path) def add_symbol(document, group, command, pos): - return inkex.etree.SubElement(group, SVG_USE_TAG, - { - "id": generate_unique_id(document, "command_use"), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(pos.x), - "y": str(pos.y), + return etree.SubElement(group, SVG_USE_TAG, + { + "id": generate_unique_id(document, "command_use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), - # l10n: the name of a command symbol (example: scissors icon for trim command) - INKSCAPE_LABEL: _("command marker"), - }) + # l10n: the name of a command symbol (example: scissors icon for trim command) + INKSCAPE_LABEL: _("command marker"), + }) def get_command_pos(element, index, total): @@ -397,14 +395,14 @@ def add_layer_commands(layer, commands): for command in commands: ensure_symbol(document, command) - inkex.etree.SubElement(layer, SVG_USE_TAG, - { - "id": generate_unique_id(document, "use"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": "0", - "y": "-10", - "transform": correction_transform - }) + etree.SubElement(layer, SVG_USE_TAG, + { + "id": generate_unique_id(document, "use"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": "0", + "y": "-10", + "transform": correction_transform + }) diff --git a/lib/debug.py b/lib/debug.py index 6ce676975..5d022e63a 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -1,17 +1,16 @@ import atexit -from contextlib import contextmanager -from datetime import datetime import os import socket import sys import time +from contextlib import contextmanager +from datetime import datetime -from inkex import etree import inkex -from simplestyle import formatStyle +from lxml import etree -from svg import line_strings_to_path -from svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL +from .svg import line_strings_to_path +from .svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL def check_enabled(func): @@ -36,7 +35,10 @@ class Debug(object): self.init_svg() def init_log(self): - self.log_file = open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.log"), "w") + self.log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.log") + # delete old content + with open(self.log_file, "w"): + pass self.log("Debug logging enabled.") def init_debugger(self): @@ -60,7 +62,7 @@ class Debug(object): try: pydevd.settrace() - except socket.error, error: + except socket.error as error: self.log("Debugging: connection to pydevd failed: %s", error) self.log("Be sure to run 'Start debugging server' in PyDev to enable debugging.") else: @@ -74,8 +76,8 @@ class Debug(object): def save_svg(self): tree = etree.ElementTree(self.svg) - with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg"), "w") as debug_svg: - tree.write(debug_svg) + debug_svg = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug.svg") + tree.write(debug_svg) @check_enabled def add_layer(self, name="Debug"): @@ -113,20 +115,21 @@ class Debug(object): timestamp = now.isoformat() self.last_log_time = now - print >> self.log_file, timestamp, message % args - self.log_file.flush() + with open(self.log_file, "a") as logfile: + print(timestamp, message % args, file=logfile) + logfile.flush() def time(self, func): def decorated(*args, **kwargs): if self.enabled: - self.raw_log("entering %s()", func.func_name) + self.raw_log("entering %s()", func.__name__) start = time.time() result = func(*args, **kwargs) if self.enabled: end = time.time() - self.raw_log("leaving %s(), duration = %s", func.func_name, round(end - start, 6)) + self.raw_log("leaving %s(), duration = %s", func.__name__, round(end - start, 6)) return result @@ -150,7 +153,7 @@ class Debug(object): @check_enabled def log_line_strings(self, line_strings, name=None, color=None): path = line_strings_to_path(line_strings) - path.set('style', formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"})) + path.set('style', str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"}))) if name is not None: path.set(INKSCAPE_LABEL, name) @@ -161,7 +164,7 @@ class Debug(object): def log_line(self, start, end, name="line", color=None): self.log_svg_element(etree.Element("path", { "d": "M%s,%s %s,%s" % (start + end), - "style": formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"}), + "style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"})), INKSCAPE_LABEL: name })) @@ -174,7 +177,7 @@ class Debug(object): self.log_svg_element(etree.Element("path", { "d": d, - "style": formatStyle({"stroke": color or "#000000", "stroke-width": "0.3"}), + "style": str(inkex.Style({"stroke": color or "#000000", "stroke-width": "0.3"})), INKSCAPE_LABEL: name })) diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index 92ef94a38..d53b2314c 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.py @@ -1,11 +1,11 @@ -from auto_fill import AutoFill -from clone import Clone -from element import EmbroideryElement -from empty_d_object import EmptyDObject -from fill import Fill -from image import ImageObject -from polyline import Polyline -from satin_column import SatinColumn -from stroke import Stroke -from text import TextObject -from utils import node_to_elements, nodes_to_elements +from .auto_fill import AutoFill +from .clone import Clone +from .element import EmbroideryElement +from .empty_d_object import EmptyDObject +from .fill import Fill +from .image import ImageObject +from .polyline import Polyline +from .satin_column import SatinColumn +from .stroke import Stroke +from .text import TextObject +from .utils import node_to_elements, nodes_to_elements diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index b574c8bf1..31da7e630 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -6,10 +6,9 @@ from shapely import geometry as shgeo from ..i18n import _ from ..stitches import auto_fill -from ..utils import cache +from ..utils import cache, version from .element import Patch, param from .fill import Fill - from .validation import ValidationWarning @@ -20,6 +19,18 @@ class SmallShapeWarning(ValidationWarning): "the outline instead.") +class ExpandWarning(ValidationWarning): + name = _("Expand") + description = _("The expand parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use original size instead.") + + +class UnderlayInsetWarning(ValidationWarning): + name = _("Inset") + description = _("The underlay inset parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use the original size instead.") + + class AutoFill(Fill): element_name = _("AutoFill") @@ -157,9 +168,13 @@ class AutoFill(Fill): def underlay_underpath(self): return self.get_boolean_param('underlay_underpath', True) - def shrink_or_grow_shape(self, amount): + def shrink_or_grow_shape(self, amount, validate=False): if amount: shape = self.shape.buffer(amount) + # changing the size can empty the shape + # in this case we want to use the original shape rather than returning an error + if shape.is_empty and not validate: + return self.shape if not isinstance(shape, shgeo.MultiPolygon): shape = shgeo.MultiPolygon([shape]) return shape @@ -235,6 +250,7 @@ class AutoFill(Fill): # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" + message += version.get_inkstitch_version() + "\n\n" message += traceback.format_exc() self.fatal(message) @@ -245,5 +261,11 @@ class AutoFill(Fill): if self.shape.area < 20: yield SmallShapeWarning(self.shape.centroid) + if self.shrink_or_grow_shape(self.expand, True).is_empty: + yield ExpandWarning(self.shape.centroid) + + if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: + yield UnderlayInsetWarning(self.shape.centroid) + for warning in super(AutoFill, self).validation_warnings(): yield warning diff --git a/lib/elements/clone.py b/lib/elements/clone.py index b8046d2d3..fd770bd7c 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -1,16 +1,13 @@ -from copy import copy from math import atan, degrees -from simpletransform import (applyTransformToNode, applyTransformToPoint, - computeBBox, parseTransform) +import inkex from ..commands import is_command, is_command_symbol from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements -from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_GROUP_TAG, - SVG_LINK_TAG, SVG_POLYLINE_TAG, SVG_USE_TAG, - XLINK_HREF) +from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, + SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF) from ..utils import cache from .auto_fill import AutoFill from .element import EmbroideryElement, param @@ -74,16 +71,16 @@ class Clone(EmbroideryElement): if node.tag == SVG_POLYLINE_TAG: return [Polyline(node)] - elif element.get_boolean_param("satin_column") and element.get_style("stroke"): + elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node): return [SatinColumn(node)] else: elements = [] - if element.get_style("fill", "black") and not element.get_style("fill-opacity", 1) == "0": + if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) else: elements.append(Fill(node)) - if element.get_style("stroke"): + if element.get_style("stroke", self.node) is not None: if not is_command(element.node): elements.append(Stroke(node)) if element.get_boolean_param("stroke_first", False): @@ -98,32 +95,8 @@ class Clone(EmbroideryElement): if source_node.tag not in EMBROIDERABLE_TAGS: return [] - clone = copy(source_node) + self.node.style = source_node.composed_style() - # set id - clone_id = 'clone__%s__%s' % (self.node.get('id', ''), clone.get('id', '')) - clone.set('id', clone_id) - - # apply transform - transform = get_node_transform(self.node) - applyTransformToNode(transform, clone) - - # apply style - stroke_style = self.get_clone_style('stroke', self.node) - if not stroke_style: - stroke_style = self.get_clone_style('stroke', source_node) - fill_style = self.node.get('fill') - if not fill_style: - fill_style = self.get_clone_style('fill', source_node, "#000000") - fill_opacity = self.node.get('fill-opacity') - if not fill_opacity: - fill_opacity = self.get_clone_style('fill-opacity', source_node, "1") - style = "fill:%s;fill-opacity:%s;" % (fill_style, fill_opacity) - if stroke_style: - style += "stroke:%s;" % stroke_style - clone.set('style', style) - - # set fill angle. Use either # a. a custom set fill angle # b. calculated rotation for the cloned fill element to look exactly as it's source param = INKSTITCH_ATTRIBS['angle'] @@ -131,48 +104,32 @@ class Clone(EmbroideryElement): angle = self.clone_fill_angle else: # clone angle - clone_mat = parseTransform(clone.get('transform', '')) + clone_mat = self.node.composed_transform() clone_angle = degrees(atan(-clone_mat[1][0]/clone_mat[1][1])) # source node angle - source_mat = parseTransform(source_node.get('transform', '')) + source_mat = source_node.composed_transform() source_angle = degrees(atan(-source_mat[1][0]/source_mat[1][1])) # source node fill angle source_fill_angle = source_node.get(param, 0) angle = clone_angle + float(source_fill_angle) - source_angle - clone.set(param, str(angle)) + self.node.set(param, str(angle)) - elements = self.clone_to_element(clone) + elements = self.clone_to_element(self.node) for element in elements: patches.extend(element.to_patches(last_patch)) return patches - def _get_clone_style_raw(self, style_name, node): - style = self.parse_style() - style = style.get(style_name) or self.node.get(style_name) - parent = self.node.getparent() - # style not found, get inherited style elements - while not style and parent is not None: - if parent.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG]: - parent = parent.getparent() - continue - style = self.parse_style(parent) - style = style.get(style_name) or parent.get(style_name) - parent = parent.getparent() - return style - def get_clone_style(self, style_name, node, default=None): - style = self._get_clone_style_raw(style_name, node) or default + style = inkex.styles.AttrFallbackStyle(node).get(style_name) or default return style def center(self, source_node): - xmin, xmax, ymin, ymax = computeBBox([source_node]) - point = [(xmax-((xmax-xmin)/2)), (ymax-((ymax-ymin)/2))] - transform = get_node_transform(self.node) - applyTransformToPoint(transform, point) - return point + transform = get_node_transform(self.node.getparent()) + center = self.node.bounding_box(transform).center + return center def validation_warnings(self): source_node = get_clone_source(self.node) diff --git a/lib/elements/element.py b/lib/elements/element.py index 5d2934cdc..2ced143b7 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -1,18 +1,16 @@ import sys from copy import deepcopy -import cubicsuperpath -import simpletransform +import inkex import tinycss2 -from cspsubdiv import cspsubdiv +from inkex import bezier -from .svg_objects import circle_to_path, ellipse_to_path, rect_to_path from ..commands import find_commands from ..i18n import _ from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length, get_node_transform) -from ..svg.tags import (EMBROIDERABLE_TAGS, INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_CIRCLE_TAG, SVG_ELLIPSE_TAG, SVG_GROUP_TAG, SVG_LINK_TAG, - SVG_OBJECT_TAGS, SVG_RECT_TAG) +from ..svg.tags import (EMBROIDERABLE_TAGS, INKSCAPE_LABEL, INKSTITCH_ATTRIBS, + SVG_GROUP_TAG, SVG_LINK_TAG, SVG_USE_TAG) from ..utils import Point, cache @@ -155,22 +153,29 @@ class EmbroideryElement(object): def parse_style(self, node=None): if node is None: node = self.node + element_style = node.get("style", "") + if element_style is None: + return None declarations = tinycss2.parse_declaration_list(node.get("style", "")) style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations} return style @cache def _get_style_raw(self, style_name): - if self.node.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG] and self.node.tag not in EMBROIDERABLE_TAGS: + if self.node is None: + return None + if self.node.tag not in [SVG_GROUP_TAG, SVG_LINK_TAG, SVG_USE_TAG] and self.node.tag not in EMBROIDERABLE_TAGS: return None style = self.parse_style() - style = style.get(style_name) or self.node.get(style_name) + if style: + style = style.get(style_name) or self.node.get(style_name) parent = self.node.getparent() # style not found, get inherited style elements while not style and parent is not None: style = self.parse_style(parent) - style = style.get(style_name) or parent.get(style_name) + if style: + style = style.get(style_name) or parent.get(style_name) parent = parent.getparent() return style @@ -196,23 +201,23 @@ class EmbroideryElement(object): # Of course, transforms may also involve rotation, skewing, and translation. # All except translation can affect how wide the stroke appears on the screen. - node_transform = get_node_transform(self.node) + node_transform = inkex.transforms.Transform(get_node_transform(self.node)) # First, figure out the translation component of the transform. Using a zero # vector completely cancels out the rotation, scale, and skew components. zero = [0, 0] - simpletransform.applyTransformToPoint(node_transform, zero) + zero = inkex.Transform.apply_to_point(node_transform, zero) translate = Point(*zero) # Next, see how the transform affects unit vectors in the X and Y axes. We # need to subtract off the translation or it will affect the magnitude of # the resulting vector, which we don't want. unit_x = [1, 0] - simpletransform.applyTransformToPoint(node_transform, unit_x) + unit_x = inkex.Transform.apply_to_point(node_transform, unit_x) sx = (Point(*unit_x) - translate).length() unit_y = [0, 1] - simpletransform.applyTransformToPoint(node_transform, unit_y) + unit_y = inkex.Transform.apply_to_point(node_transform, unit_y) sy = (Point(*unit_y) - translate).length() # Take the average as a best guess. @@ -223,11 +228,7 @@ class EmbroideryElement(object): @property @cache def stroke_width(self): - width = self.get_style("stroke-width", None) - - if width is None: - return 1.0 - + width = self.get_style("stroke-width", "1.0") width = convert_length(width) return width * self.stroke_scale @@ -271,20 +272,15 @@ class EmbroideryElement(object): # In a path, each element in the 3-tuple is itself a tuple of (x, y). # Tuples all the way down. Hasn't anyone heard of using classes? - if self.node.tag in SVG_OBJECT_TAGS: - if self.node.tag == SVG_RECT_TAG: - d = rect_to_path(self.node) - elif self.node.tag == SVG_ELLIPSE_TAG: - d = ellipse_to_path(self.node) - elif self.node.tag == SVG_CIRCLE_TAG: - d = circle_to_path(self.node) + if getattr(self.node, "get_path", None): + d = self.node.get_path() else: d = self.node.get("d", "") if not d: self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id"))) - return cubicsuperpath.parsePath(d) + return inkex.paths.Path(d).to_superpath() @cache def parse_path(self): @@ -315,7 +311,7 @@ class EmbroideryElement(object): return commands[0] elif len(commands) > 1: raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") % - dict(id=self.node.get(id), command=command)) + dict(id=self.node.get('id'), command=command)) else: return None @@ -326,13 +322,13 @@ class EmbroideryElement(object): """approximate a path containing beziers with a series of points""" path = deepcopy(path) - cspsubdiv(path, 0.1) + bezier.cspsubdiv(path, 0.1) return [self.strip_control_points(subpath) for subpath in path] def flatten_subpath(self, subpath): path = [deepcopy(subpath)] - cspsubdiv(path, 0.1) + bezier.cspsubdiv(path, 0.1) return self.strip_control_points(path[0]) @@ -373,7 +369,7 @@ class EmbroideryElement(object): # L10N used when showing an error message to the user such as # "Some Path (path1234): error: satin column: One or more of the rungs doesn't intersect both rails." error_msg = "%s: %s %s" % (name, _("error:"), message) - print >> sys.stderr, "%s" % (error_msg.encode("UTF-8")) + inkex.errormsg(error_msg) sys.exit(1) def validation_errors(self): diff --git a/lib/elements/fill.py b/lib/elements/fill.py index 2e94847a3..1f4c7b1e7 100644 --- a/lib/elements/fill.py +++ b/lib/elements/fill.py @@ -136,6 +136,12 @@ class Fill(EmbroideryElement): # biggest path. paths = self.paths paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) # There is a great number of "crossing border" errors on fill shapes diff --git a/lib/elements/image.py b/lib/elements/image.py index ec8d1765c..160898a5b 100644 --- a/lib/elements/image.py +++ b/lib/elements/image.py @@ -1,7 +1,5 @@ -from simpletransform import applyTransformToPoint - from ..i18n import _ -from ..svg import get_node_transform +from ..svg.path import get_node_transform from .element import EmbroideryElement from .validation import ObjectTypeWarning @@ -19,13 +17,9 @@ class ImageTypeWarning(ObjectTypeWarning): class ImageObject(EmbroideryElement): def center(self): - point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))] - point = [(point[0]+(float(self.node.get('width', 0))/2)), (point[1]+(float(self.node.get('height', 0))/2))] - - transform = get_node_transform(self.node) - applyTransformToPoint(transform, point) - - return point + transform = get_node_transform(self.node.getparent()) + center = self.node.bounding_box(transform).center + return center def validation_warnings(self): yield ImageTypeWarning(self.center()) diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py index 2d008d357..da1e807df 100644 --- a/lib/elements/polyline.py +++ b/lib/elements/polyline.py @@ -1,3 +1,4 @@ +from inkex import Path from shapely import geometry as shgeo from ..i18n import _ @@ -36,7 +37,7 @@ class Polyline(EmbroideryElement): @property @param('polyline', _('Manual stitch along path'), type='toggle', inverse=True) - def satin_column(self): + def polyline(self): return self.get_boolean_param("polyline") @property @@ -61,8 +62,8 @@ class Polyline(EmbroideryElement): # svg transforms that is in our superclass, we'll convert the polyline # to a degenerate cubic superpath in which the bezier handles are on # the segment endpoints. - - path = [[[point[:], point[:], point[:]] for point in self.points]] + path = self.node.get_path() + path = Path(path).to_superpath() return path diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index d0a3f8ffe..fbadd92ff 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -1,14 +1,15 @@ from copy import deepcopy -from itertools import chain, izip +from itertools import chain -import cubicsuperpath -from shapely import affinity as shaffinity, geometry as shgeo +from inkex import paths +from shapely import affinity as shaffinity +from shapely import geometry as shgeo -from .element import EmbroideryElement, Patch, param -from .validation import ValidationError from ..i18n import _ from ..svg import line_strings_to_csp, point_lists_to_csp from ..utils import Point, cache, collapse_duplicate_point, cut +from .element import EmbroideryElement, Patch, param +from .validation import ValidationError class SatinHasFillError(ValidationError): @@ -234,7 +235,7 @@ class SatinColumn(EmbroideryElement): rung_endpoints.append(points) rungs = [] - for start, end in izip(*rung_endpoints): + for start, end in zip(*rung_endpoints): # Expand the points just a bit to ensure that shapely thinks they # intersect with the rails even with floating point inaccuracy. start = Point(*start) @@ -266,12 +267,12 @@ class SatinColumn(EmbroideryElement): if num_paths <= 2: # old-style satin column with no rungs - return range(num_paths) + return list(range(num_paths)) # This takes advantage of the fact that sum() counts True as 1 - intersection_counts = [sum(paths[i].intersects(paths[j]) for j in xrange(num_paths) if i != j) - for i in xrange(num_paths)] - paths_not_intersecting_two = [i for i in xrange(num_paths) if intersection_counts[i] != 2] + intersection_counts = [sum(paths[i].intersects(paths[j]) for j in range(num_paths) if i != j) + for i in range(num_paths)] + paths_not_intersecting_two = [i for i in range(num_paths) if intersection_counts[i] != 2] num_not_intersecting_two = len(paths_not_intersecting_two) if num_not_intersecting_two == 2: @@ -292,7 +293,7 @@ class SatinColumn(EmbroideryElement): # kind of weird thing. Maybe one of the rungs crosses a rail more # than once. Treat it like the previous case and we'll sort out # the intersection issues later. - indices_by_length = sorted(range(num_paths), key=lambda index: paths[index].length, reverse=True) + indices_by_length = sorted(list(range(num_paths)), key=lambda index: paths[index].length, reverse=True) return indices_by_length[:2] def _cut_rail(self, rail, rung): @@ -330,7 +331,7 @@ class SatinColumn(EmbroideryElement): self._cut_rail(rail, rung) for rail in rails: - for i in xrange(len(rail)): + for i in range(len(rail)): if rail[i] is not None: rail[i] = [Point(*coord) for coord in rail[i].coords] @@ -345,7 +346,7 @@ class SatinColumn(EmbroideryElement): # zero-length bezier at the star. The user's goal here is to ignore the # horizontal section of the right rail. - sections = zip(*rails) + sections = list(zip(*rails)) sections = [s for s in sections if s[0] is not None and s[1] is not None] return sections @@ -438,13 +439,13 @@ class SatinColumn(EmbroideryElement): """ # like in do_satin() - points = list(chain.from_iterable(izip(*self.plot_points_on_rails(self.zigzag_spacing, 0)))) + points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing, 0)))) if isinstance(split_point, float): index_of_closest_stitch = int(round(len(points) * split_point)) else: split_point = Point(*split_point) - index_of_closest_stitch = min(range(len(points)), key=lambda index: split_point.distance(points[index])) + index_of_closest_stitch = min(list(range(len(points))), key=lambda index: split_point.distance(points[index])) if index_of_closest_stitch % 2 == 0: # split point is on the first rail @@ -517,7 +518,7 @@ class SatinColumn(EmbroideryElement): def _csp_to_satin(self, csp): node = deepcopy(self.node) - d = cubicsuperpath.formatPath(csp) + d = paths.CubicSuperPath(csp).to_path() node.set("d", d) # we've already applied the transform, so get rid of it @@ -626,7 +627,9 @@ class SatinColumn(EmbroideryElement): # Each iteration of this outer loop is one stitch. Keep going # until we fall off the end of the section. - old_center = (pos0 + pos1) / 2.0 + # TODO: is there an other way? + # old_center = (pos0 + pos1) / 2.0 + old_center = shgeo.Point(x/2 for x in (pos0 + pos1)) while to_travel > 0 and index0 < last_index0 and index1 < last_index1: # In this loop, we inch along each rail a tiny bit per @@ -653,7 +656,9 @@ class SatinColumn(EmbroideryElement): pos0, index0 = self.walk(section0, pos0, index0, 0.05) pos1, index1 = self.walk(section1, pos1, index1, 0.05 * ratio) - new_center = (pos0 + pos1) / 2.0 + # TODO: is there a better way? + # new_center = (pos0 + pos1) / 2.0 + new_center = shgeo.Point(x/2 for x in (pos0 + pos1)) to_travel -= new_center.distance(old_center) old_center = new_center @@ -705,7 +710,7 @@ class SatinColumn(EmbroideryElement): # This fancy bit of iterable magic just repeatedly takes a point # from each side in turn. - for point in chain.from_iterable(izip(*sides)): + for point in chain.from_iterable(zip(*sides)): patch.add_stitch(point) return patch @@ -724,7 +729,7 @@ class SatinColumn(EmbroideryElement): sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation) # Like in zigzag_underlay(): take a point from each side in turn. - for point in chain.from_iterable(izip(*sides)): + for point in chain.from_iterable(zip(*sides)): patch.add_stitch(point) return patch @@ -743,7 +748,7 @@ class SatinColumn(EmbroideryElement): # "left" and "right" here are kind of arbitrary designations meaning # a point from the first and second rail repectively - for left, right in izip(*sides): + for left, right in zip(*sides): patch.add_stitch(left) patch.add_stitch(right) patch.add_stitch(left) diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 36d1048e5..d63a21a91 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -134,9 +134,9 @@ class Stroke(EmbroideryElement): global warned_about_legacy_running_stitch if not warned_about_legacy_running_stitch: warned_about_legacy_running_stitch = True - print >> sys.stderr, _("Legacy running stitch setting detected!\n\nIt looks like you're using a stroke " + - "smaller than 0.5 units to indicate a running stitch, which is deprecated. Instead, please set " + - "your stroke to be dashed to indicate running stitch. Any kind of dash will work.") + print(_("Legacy running stitch setting detected!\n\nIt looks like you're using a stroke " + + "smaller than 0.5 units to indicate a running stitch, which is deprecated. Instead, please set " + + "your stroke to be dashed to indicate running stitch. Any kind of dash will work."), file=sys.stderr) # still allow the deprecated setting to work in order to support old files return True @@ -157,7 +157,7 @@ class Stroke(EmbroideryElement): offset = stroke_width / 2.0 - for i in xrange(len(patch) - 1): + for i in range(len(patch) - 1): start = patch.stitches[i] end = patch.stitches[i + 1] segment_direction = (end - start).unit() @@ -174,7 +174,7 @@ class Stroke(EmbroideryElement): repeated_path = [] # go back and forth along the path as specified by self.repeats - for i in xrange(self.repeats): + for i in range(self.repeats): if i % 2 == 1: # reverse every other pass this_path = path[::-1] diff --git a/lib/elements/svg_objects.py b/lib/elements/svg_objects.py deleted file mode 100644 index 4760af5f4..000000000 --- a/lib/elements/svg_objects.py +++ /dev/null @@ -1,71 +0,0 @@ -def rect_to_path(node): - x = float(node.get('x', '0')) - y = float(node.get('y', '0')) - width = float(node.get('width', '0')) - height = float(node.get('height', '0')) - rx = 0 - ry = 0 - - # rounded corners - # the following rules apply for radius calculations: - # * if rx or ry is missing it has to take the value of the other one - # * the radius cannot be bigger than half of the corresponding side - # (otherwise we receive an invalid path) - if node.get('rx') or node.get('ry'): - if node.get('rx'): - rx = float(node.get('rx', '0')) - ry = rx - if node.get('ry'): - ry = float(node.get('ry', '0')) - if not ry: - ry = rx - - rx = min(width/2, rx) - ry = min(height/2, ry) - - path = 'M %(startx)f,%(y)f ' \ - 'h %(width)f ' \ - 'q %(rx)f,0 %(rx)f,%(ry)f ' \ - 'v %(height)f ' \ - 'q 0,%(ry)f -%(rx)f,%(ry)f ' \ - 'h -%(width)f ' \ - 'q -%(rx)f,0 -%(rx)f,-%(ry)f ' \ - 'v -%(height)f ' \ - 'q 0,-%(ry)f %(rx)f,-%(ry)f ' \ - 'Z' \ - % dict(startx=x+rx, x=x, y=y, width=width-(2*rx), height=height-(2*ry), rx=rx, ry=ry) - - else: - path = "M %f,%f H %f V %f H %f Z" % (x, y, width+x, height+y, x) - - return path - - -def ellipse_to_path(node): - rx = float(node.get('rx', "0")) or float(node.get('r', "0")) - ry = float(node.get('ry', "0")) or float(node.get('r', "0")) - cx = float(node.get('cx')) - cy = float(node.get('cy')) - - path = 'M %(cx_r)f,%(cy)f' \ - 'C %(cx_r)f,%(cy_r)f %(cx)f,%(cy_r)f %(cx)f,%(cy_r)f ' \ - '%(cxr)f,%(cy_r)f %(cxr)f,%(cy)f %(cxr)f,%(cy)f ' \ - '%(cxr)f,%(cyr)f %(cx)f,%(cyr)f %(cx)f,%(cyr)f ' \ - '%(cx_r)f,%(cyr)f %(cx_r)f,%(cy)f %(cx_r)f,%(cy)f ' \ - 'Z' \ - % dict(cx=cx, cx_r=cx-rx, cxr=cx+rx, cy=cy, cyr=cy+ry, cy_r=cy-ry) - - return path - - -def circle_to_path(node): - cx = float(node.get('cx')) - cy = float(node.get('cy')) - r = float(node.get('r')) - - path = 'M %(xstart)f, %(cy)f ' \ - 'a %(r)f,%(r)f 0 1,0 %(rr)f,0 ' \ - 'a %(r)f,%(r)f 0 1,0 -%(rr)f,0 ' \ - % dict(xstart=(cx-r), cy=cy, r=r, rr=(r*2)) - - return path diff --git a/lib/elements/text.py b/lib/elements/text.py index 2d066bb03..838be96ae 100644 --- a/lib/elements/text.py +++ b/lib/elements/text.py @@ -1,9 +1,7 @@ -from simpletransform import applyTransformToPoint - from ..i18n import _ -from ..svg import get_node_transform from .element import EmbroideryElement from .validation import ObjectTypeWarning +from ..svg.path import get_node_transform class TextTypeWarning(ObjectTypeWarning): @@ -17,16 +15,14 @@ class TextTypeWarning(ObjectTypeWarning): class TextObject(EmbroideryElement): - def center(self): - point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))] - - transform = get_node_transform(self.node) - applyTransformToPoint(transform, point) + def pointer(self): + transform = get_node_transform(self.node.getparent()) + point = self.node.bounding_box(transform).center return point def validation_warnings(self): - yield TextTypeWarning(self.center()) + yield TextTypeWarning(self.pointer()) def to_patches(self, last_patch): return [] diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index a5388f196..1758772ee 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -1,28 +1,32 @@ -from auto_satin import AutoSatin -from break_apart import BreakApart -from cleanup import Cleanup -from convert_to_satin import ConvertToSatin -from cut_satin import CutSatin -from embroider import Embroider -from flip import Flip -from global_commands import GlobalCommands -from import_threadlist import ImportThreadlist -from input import Input -from install import Install -from layer_commands import LayerCommands -from lettering import Lettering from lib.extensions.troubleshoot import Troubleshoot -from object_commands import ObjectCommands -from output import Output -from params import Params -from print_pdf import Print -from remove_embroidery_settings import RemoveEmbroiderySettings -from simulator import Simulator -from stitch_plan_preview import StitchPlanPreview -from zip import Zip -__all__ = extensions = [Embroider, - StitchPlanPreview, +from .auto_satin import AutoSatin +from .break_apart import BreakApart +from .cleanup import Cleanup +from .convert_to_satin import ConvertToSatin +from .cut_satin import CutSatin +from .flip import Flip +from .global_commands import GlobalCommands +from .import_threadlist import ImportThreadlist +from .input import Input +from .install import Install +from .layer_commands import LayerCommands +from .lettering import Lettering +from .object_commands import ObjectCommands +from .output import Output +from .params import Params +from .print_pdf import Print +from .remove_embroidery_settings import RemoveEmbroiderySettings +from .reorder import Reorder +from .simulator import Simulator +from .stitch_plan_preview import StitchPlanPreview +from .zip import Zip +from .lettering_generate_json import LetteringGenerateJson +from .lettering_remove_kerning import LetteringRemoveKerning +from .lettering_custom_font_dir import LetteringCustomFontDir +from .embroider_settings import EmbroiderSettings + +__all__ = extensions = [StitchPlanPreview, Install, Params, Print, @@ -37,9 +41,14 @@ __all__ = extensions = [Embroider, CutSatin, AutoSatin, Lettering, + LetteringGenerateJson, + LetteringRemoveKerning, + LetteringCustomFontDir, Troubleshoot, RemoveEmbroiderySettings, Cleanup, BreakApart, ImportThreadlist, - Simulator] + Simulator, + Reorder, + EmbroiderSettings] diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index a447a493a..fce4a1fd0 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -14,7 +14,7 @@ class AutoSatin(CommandsExtension): def __init__(self, *args, **kwargs): CommandsExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-p", "--preserve_order", dest="preserve_order", type="inkbool", default=False) + self.arg_parser.add_argument("-p", "--preserve_order", dest="preserve_order", type=inkex.Boolean, default=False) def get_starting_point(self): return self.get_point("satin_start") @@ -39,7 +39,7 @@ class AutoSatin(CommandsExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: # L10N auto-route satin columns extension inkex.errormsg(_("Please select one or more satin columns.")) return False diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 9f6dc5f6a..1a38973f7 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -1,21 +1,19 @@ import json import os import re -from collections import MutableMapping -from copy import deepcopy - -from stringcase import snakecase +from collections.abc import MutableMapping import inkex +from lxml import etree +from stringcase import snakecase from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements -from ..elements.clone import is_clone, is_embroiderable_clone +from ..elements.clone import is_clone from ..i18n import _ from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG, - SVG_PATH_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -71,7 +69,7 @@ class InkStitchMetadata(MutableMapping): tag = inkex.addNS(name, "inkstitch") item = self.metadata.find(tag) if item is None and create: - item = inkex.etree.SubElement(self.metadata, tag) + item = etree.SubElement(self.metadata, tag) return item @@ -117,7 +115,7 @@ class InkstitchExtension(inkex.Effect): def ensure_current_layer(self): # if no layer is selected, inkex defaults to the root, which isn't # particularly useful - if self.current_layer is self.document.getroot(): + if self.svg.get_current_layer() is self.document.getroot(): try: self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0] except IndexError: @@ -125,7 +123,7 @@ class InkstitchExtension(inkex.Effect): pass def no_elements_error(self): - if self.selected: + if self.svg.selected: # l10n This was previously: "No embroiderable paths selected." inkex.errormsg(_("Ink/Stitch doesn't know how to work with any of the objects you've selected.") + "\n") else: @@ -154,8 +152,8 @@ class InkstitchExtension(inkex.Effect): if is_command(node) or node.get(CONNECTOR_TYPE): return[] - if self.selected: - if node.get("id") in self.selected: + if self.svg.selected: + if node.get("id") in self.svg.selected: selected = True else: # if the user didn't select anything that means we process everything @@ -165,7 +163,7 @@ class InkstitchExtension(inkex.Effect): nodes.extend(self.descendants(child, selected, troubleshoot)) if selected: - if (node.tag in EMBROIDERABLE_TAGS or is_embroiderable_clone(node)) and not (node.tag == SVG_PATH_TAG and not node.get('d', '')): + if getattr(node, "get_path", None): nodes.append(node) elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or node.tag in EMBROIDERABLE_TAGS or is_clone(node)): nodes.append(node) @@ -206,24 +204,3 @@ class InkstitchExtension(inkex.Effect): def uniqueId(self, prefix, make_new_id=True): """Override inkex.Effect.uniqueId with a nicer naming scheme.""" return generate_unique_id(self.document, prefix) - - def parse(self): - """Override inkex.Effect.parse to add Ink/Stitch xml namespace""" - - # SVG parsers don't actually look for anything at this URL. They just - # care that it's unique. That defines a "namespace" of element and - # attribute names to disambiguate conflicts with element and - # attribute names other XML namespaces. - - # call the superclass's method first - inkex.Effect.parse(self) - - # Add the inkstitch namespace to the SVG. The inkstitch namespace is - # added to inkex.NSS in ../svg/tags.py at import time. - - # The below is the only way I could find to add a namespace to an - # existing element tree at the top without getting ugly prefixes like "ns0". - inkex.etree.cleanup_namespaces(self.document, - top_nsmap=inkex.NSS, - keep_ns_prefixes=inkex.NSS.keys()) - self.original_document = deepcopy(self.document) diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py index 0b17d3d7c..d0ab2619f 100644 --- a/lib/extensions/break_apart.py +++ b/lib/extensions/break_apart.py @@ -1,11 +1,10 @@ import logging from copy import copy +import inkex from shapely.geometry import LineString, MultiPolygon, Polygon from shapely.ops import polygonize, unary_union -import inkex - from ..elements import EmbroideryElement from ..i18n import _ from ..svg import get_correction_transform @@ -19,10 +18,11 @@ class BreakApart(InkstitchExtension): ''' def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method") + self.arg_parser.add_argument("-m", "--method", type=int, default=1, dest="method") + self.minimum_size = 5 def effect(self): # noqa: C901 - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more fill areas to break apart.")) return @@ -41,13 +41,12 @@ class BreakApart(InkstitchExtension): try: paths.sort(key=lambda point_list: Polygon(point_list).area, reverse=True) polygon = MultiPolygon([(paths[0], paths[1:])]) - if self.geom_is_valid(polygon): + if self.geom_is_valid(polygon) and Polygon(paths[-1]).area > self.minimum_size: continue except ValueError: pass polygons = self.break_apart_paths(paths) - polygons = self.ensure_minimum_size(polygons, 5) if self.options.method == 1: polygons = self.combine_overlapping_polygons(polygons) polygons = self.recombine_polygons(polygons) @@ -106,6 +105,7 @@ class BreakApart(InkstitchExtension): polygons.sort(key=lambda polygon: polygon.area, reverse=True) multipolygons = [] holes = [] + self.ensure_minimum_size(polygons, self.minimum_size) for polygon in polygons: if polygon in holes: continue diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index e06b4bea5..f1965abac 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -1,6 +1,4 @@ -import sys - -from inkex import NSS +from inkex import NSS, Boolean, errormsg from ..elements import Fill, Stroke from ..i18n import _ @@ -10,10 +8,10 @@ from .base import InkstitchExtension class Cleanup(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-f", "--rm_fill", dest="rm_fill", type="inkbool", default=True) - self.OptionParser.add_option("-s", "--rm_stroke", dest="rm_stroke", type="inkbool", default=True) - self.OptionParser.add_option("-a", "--fill_threshold", dest="fill_threshold", type="int", default=20) - self.OptionParser.add_option("-l", "--stroke_threshold", dest="stroke_threshold", type="int", default=5) + self.arg_parser.add_argument("-f", "--rm_fill", dest="rm_fill", type=Boolean, default=True) + self.arg_parser.add_argument("-s", "--rm_stroke", dest="rm_stroke", type=Boolean, default=True) + self.arg_parser.add_argument("-a", "--fill_threshold", dest="fill_threshold", type=int, default=20) + self.arg_parser.add_argument("-l", "--stroke_threshold", dest="stroke_threshold", type=int, default=5) def effect(self): self.rm_fill = self.options.rm_fill @@ -21,8 +19,7 @@ class Cleanup(InkstitchExtension): self.fill_threshold = self.options.fill_threshold self.stroke_threshold = self.options.stroke_threshold - # Remove selection, we want every element in the document - self.selected = {} + self.svg.selected.clear() count = 0 svg = self.document.getroot() @@ -32,7 +29,7 @@ class Cleanup(InkstitchExtension): count += 1 if not self.get_elements(): - print >> sys.stderr, _("%s elements removed" % count) + errormsg(_("%s elements removed" % count)) return for element in self.elements: @@ -44,4 +41,4 @@ class Cleanup(InkstitchExtension): element.node.getparent().remove(element.node) count += 1 - print >> sys.stderr, _("%s elements removed" % count) + errormsg(_("%s elements removed" % count)) diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 86e291fd5..19b85e6d3 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -1,3 +1,5 @@ +from inkex import Boolean + from .base import InkstitchExtension @@ -7,4 +9,4 @@ class CommandsExtension(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) for command in self.COMMANDS: - self.OptionParser.add_option("--%s" % command, type="inkbool") + self.arg_parser.add_argument("--%s" % command, type=Boolean) diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index e2b287dd0..048c08da4 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -1,12 +1,13 @@ import math +import sys from itertools import chain, groupby +import inkex import numpy +from lxml import etree from numpy import diff, setdiff1d, sign from shapely import geometry as shgeo -import inkex - from ..elements import Stroke from ..i18n import _ from ..svg import PIXELS_PER_MM, get_correction_transform @@ -26,7 +27,7 @@ class ConvertToSatin(InkstitchExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select at least one line to convert to a satin column.")) return @@ -120,8 +121,15 @@ class ConvertToSatin(InkstitchExtension): path = shgeo.LineString(path) - left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args) - right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args) + try: + left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args) + right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args) + except ValueError: + # TODO: fix this error automatically + # Error reference: https://github.com/inkstitch/inkstitch/issues/964 + inkex.errormsg(_("Ink/Stitch cannot convert your stroke into a satin column. " + "Please break up your path and try again.") + '\n') + sys.exit(1) if not isinstance(left_rail, shgeo.LineString) or \ not isinstance(right_rail, shgeo.LineString): @@ -304,12 +312,11 @@ class ConvertToSatin(InkstitchExtension): d += "%s,%s " % (x, y) d += " " - return inkex.etree.Element(SVG_PATH_TAG, - { - "id": self.uniqueId("path"), - "style": path_style, - "transform": correction_transform, - "d": d, - INKSTITCH_ATTRIBS['satin_column']: "true", - } - ) + return etree.Element(SVG_PATH_TAG, + { + "id": self.uniqueId("path"), + "style": path_style, + "transform": correction_transform, + "d": d, + INKSTITCH_ATTRIBS['satin_column']: "true", + }) diff --git a/lib/extensions/cut_satin.py b/lib/extensions/cut_satin.py index b776a68c8..7cc802953 100644 --- a/lib/extensions/cut_satin.py +++ b/lib/extensions/cut_satin.py @@ -1,9 +1,9 @@ import inkex -from .base import InkstitchExtension -from ..i18n import _ from ..elements import SatinColumn +from ..i18n import _ from ..svg import get_correction_transform +from .base import InkstitchExtension class CutSatin(InkstitchExtension): @@ -11,7 +11,7 @@ class CutSatin(InkstitchExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more satin columns to cut.")) return diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py deleted file mode 100644 index b90896120..000000000 --- a/lib/extensions/embroider.py +++ /dev/null @@ -1,87 +0,0 @@ -import os - -from ..i18n import _ -from ..output import write_embroidery_file -from ..stitch_plan import patches_to_stitch_plan -from ..svg import render_stitch_plan, PIXELS_PER_MM -from .base import InkstitchExtension - - -class Embroider(InkstitchExtension): - def __init__(self, *args, **kwargs): - InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-c", "--collapse_len_mm", - action="store", type="float", - dest="collapse_length_mm", default=3.0, - help="max collapse length (mm)") - self.OptionParser.add_option("--hide_layers", - action="store", type="choice", - choices=["true", "false"], - dest="hide_layers", default="true", - help="Hide all other layers when the embroidery layer is generated") - self.OptionParser.add_option("-O", "--output_format", - action="store", type="string", - dest="output_format", default="csv", - help="Output file extenstion (default: csv)") - self.OptionParser.add_option("-P", "--path", - action="store", type="string", - dest="path", default=".", - help="Directory in which to store output file") - self.OptionParser.add_option("-F", "--output-file", - action="store", type="string", - dest="output_file", - help="Output filename.") - self.OptionParser.add_option("-b", "--max-backups", - action="store", type="int", - dest="max_backups", default=5, - help="Max number of backups of output files to keep.") - self.OptionParser.usage += _("\n\nSeeing a 'no such option' message? Please restart Inkscape to fix.") - - def get_output_path(self): - if self.options.output_file: - # This is helpful for folks that run the embroider extension - # manually from the command line (without Inkscape) for - # debugging purposes. - output_path = os.path.join(os.path.expanduser(os.path.expandvars(self.options.path.decode("UTF-8"))), - self.options.output_file.decode("UTF-8")) - else: - csv_filename = '%s.%s' % (self.get_base_file_name(), self.options.output_format) - output_path = os.path.join(self.options.path.decode("UTF-8"), csv_filename) - - def add_suffix(path, suffix): - if suffix > 0: - path = "%s.%s" % (path, suffix) - - return path - - def move_if_exists(path, suffix=0): - source = add_suffix(path, suffix) - - if suffix >= self.options.max_backups: - return - - dest = add_suffix(path, suffix + 1) - - if os.path.exists(source): - move_if_exists(path, suffix + 1) - - if os.path.exists(dest): - os.remove(dest) - - os.rename(source, dest) - - move_if_exists(output_path) - - return output_path - - def effect(self): - if not self.get_elements(): - return - - if self.options.hide_layers: - self.hide_all_layers() - - patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) - write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot()) - render_stitch_plan(self.document.getroot(), stitch_plan) diff --git a/lib/extensions/embroider_settings.py b/lib/extensions/embroider_settings.py new file mode 100644 index 000000000..88e2ba9b5 --- /dev/null +++ b/lib/extensions/embroider_settings.py @@ -0,0 +1,17 @@ +from .base import InkstitchExtension + + +class EmbroiderSettings(InkstitchExtension): + ''' + This saves embroider settings into the metadata of the file + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-c", "--collapse_len_mm", + action="store", type=float, + dest="collapse_length_mm", default=3.0, + help="max collapse length (mm)") + + def effect(self): + self.metadata = self.get_inkstitch_metadata() + self.metadata['collapse_len_mm'] = self.options.collapse_length_mm diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py index 0864da851..87b8b3f0c 100644 --- a/lib/extensions/flip.py +++ b/lib/extensions/flip.py @@ -1,9 +1,8 @@ import inkex -import cubicsuperpath -from .base import InkstitchExtension -from ..i18n import _ from ..elements import SatinColumn +from ..i18n import _ +from .base import InkstitchExtension class Flip(InkstitchExtension): @@ -14,13 +13,13 @@ class Flip(InkstitchExtension): first, second = satin.rail_indices csp[first], csp[second] = csp[second], csp[first] - satin.node.set("d", cubicsuperpath.formatPath(csp)) + satin.node.set("d", inkex.paths.CubicSuperPath.to_path(csp)) def effect(self): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more satin columns to flip.")) return diff --git a/lib/extensions/import_threadlist.py b/lib/extensions/import_threadlist.py index d31c0d69b..029043c2c 100644 --- a/lib/extensions/import_threadlist.py +++ b/lib/extensions/import_threadlist.py @@ -12,20 +12,23 @@ from .base import InkstitchExtension class ImportThreadlist(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-f", "--filepath", type="str", default="", dest="filepath") - self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method") - self.OptionParser.add_option("-t", "--palette", type="str", default=None, dest="palette") + self.arg_parser.add_argument("-f", "--filepath", type=str, default="", dest="filepath") + self.arg_parser.add_argument("-m", "--method", type=int, default=1, dest="method") + self.arg_parser.add_argument("-t", "--palette", type=str, default=None, dest="palette") def effect(self): # Remove selection, we want all the elements in the document - self.selected = {} + self.svg.selected.clear() if not self.get_elements(): return path = self.options.filepath if not os.path.exists(path): - print >> sys.stderr, _("File not found.") + inkex.errormsg(_("File not found.")) + sys.exit(1) + if os.path.isdir(path): + inkex.errormsg(_("The filepath specified is not a file but a dictionary.\nPlease choose a threadlist file to import.")) sys.exit(1) method = self.options.method @@ -35,11 +38,11 @@ class ImportThreadlist(InkstitchExtension): colors = self.parse_threadlist_by_catalog_number(path) if all(c is None for c in colors): - print >>sys.stderr, _("Couldn't find any matching colors in the file.") + inkex.errormsg(_("Couldn't find any matching colors in the file.")) if method == 1: - print >>sys.stderr, _('Please try to import as "other threadlist" and specify a color palette below.') + inkex.errormsg(_('Please try to import as "other threadlist" and specify a color palette below.')) else: - print >>sys.stderr, _("Please chose an other color palette for your design.") + inkex.errormsg(_("Please chose an other color palette for your design.")) sys.exit(1) # Iterate through the color blocks to apply colors diff --git a/lib/extensions/input.py b/lib/extensions/input.py index 957d355cc..c6dcb698d 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -1,8 +1,9 @@ import os -import pyembroidery -from inkex import etree import inkex +from lxml import etree + +import pyembroidery from ..stitch_plan import StitchPlan from ..svg import PIXELS_PER_MM, render_stitch_plan @@ -10,7 +11,7 @@ from ..svg.tags import INKSCAPE_LABEL class Input(object): - def affect(self, args): + def run(self, args): embroidery_file = args[0] pattern = pyembroidery.read(embroidery_file) @@ -47,11 +48,11 @@ class Input(object): # rename the Stitch Plan layer so that it doesn't get overwritten by Embroider layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file.decode("UTF-8"))) + layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file)) layer.attrib.pop('id') # Shift the design so that its origin is at the center of the canvas # Note: this is NOT the same as centering the design in the canvas! layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - print etree.tostring(svg) + print(etree.tostring(svg).decode('utf-8')) diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py index e710e3515..89726510e 100644 --- a/lib/extensions/layer_commands.py +++ b/lib/extensions/layer_commands.py @@ -1,9 +1,10 @@ import inkex +from lxml import etree -from ..commands import LAYER_COMMANDS, get_command_description, ensure_symbol +from ..commands import LAYER_COMMANDS, ensure_symbol, get_command_description from ..i18n import _ from ..svg import get_correction_transform -from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF +from ..svg.tags import INKSCAPE_LABEL, SVG_USE_TAG, XLINK_HREF from .commands import CommandsExtension @@ -17,20 +18,19 @@ class LayerCommands(CommandsExtension): inkex.errormsg(_("Please choose one or more commands to add.")) return - self.ensure_current_layer() - correction_transform = get_correction_transform(self.current_layer, child=True) + correction_transform = get_correction_transform(self.svg.get_current_layer(), child=True) for i, command in enumerate(commands): ensure_symbol(self.document, command) - inkex.etree.SubElement(self.current_layer, SVG_USE_TAG, - { - "id": self.uniqueId("use"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(i * 20), - "y": "-10", - "transform": correction_transform - }) + etree.SubElement(self.svg.get_current_layer(), SVG_USE_TAG, + { + "id": self.uniqueId("use"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(i * 20), + "y": "-10", + "transform": correction_transform + }) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index d988778d7..ee0dd9a0b 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -1,14 +1,12 @@ -# -*- coding: UTF-8 -*- - import json import os import sys -from base64 import b64decode, b64encode import appdirs import inkex import wx import wx.adv +from lxml import etree from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview, info_dialog @@ -19,6 +17,7 @@ from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG, SVG_PATH_TAG) from ..utils import DotDict, cache, get_bundled_dir from .commands import CommandsExtension +from .lettering_custom_font_dir import get_custom_font_dir class LetteringFrame(wx.Frame): @@ -45,6 +44,7 @@ class LetteringFrame(wx.Frame): # font details self.font_description = wx.StaticText(self, wx.ID_ANY) + self.Bind(wx.EVT_SIZE, self.resize) # options self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) @@ -80,7 +80,7 @@ class LetteringFrame(wx.Frame): """Load the settings saved into the SVG group element""" self.settings = DotDict({ - "text": u"", + "text": "", "back_and_forth": False, "font": None, "scale": 100 @@ -88,7 +88,7 @@ class LetteringFrame(wx.Frame): try: if INKSTITCH_LETTERING in self.group.attrib: - self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + self.settings.update(json.loads(self.group.get(INKSTITCH_LETTERING))) return except (TypeError, ValueError): pass @@ -103,22 +103,14 @@ class LetteringFrame(wx.Frame): def save_settings(self): """Save the settings into the SVG group element.""" - - # We base64 encode the string before storing it in an XML attribute. - # In theory, lxml should properly html-encode the string, using HTML - # entities like as necessary. However, we've found that Inkscape - # incorrectly interpolates the HTML entities upon reading the - # extension's output, rather than leaving them as is. - # - # Details: - # https://bugs.launchpad.net/inkscape/+bug/1804346 - self.group.set(INKSTITCH_LETTERING, b64encode(json.dumps(self.settings))) + self.group.set(INKSTITCH_LETTERING, json.dumps(self.settings)) def update_font_list(self): font_paths = { get_bundled_dir("fonts"), os.path.expanduser("~/.inkstitch/fonts"), os.path.join(appdirs.user_config_dir('inkstitch'), 'fonts'), + get_custom_font_dir() } self.fonts = {} @@ -130,13 +122,12 @@ class LetteringFrame(wx.Frame): except OSError: continue - try: - for font_dir in font_dirs: - font = Font(os.path.join(font_path, font_dir)) - self.fonts[font.name] = font - self.fonts_by_id[font.id] = font - except FontError: - pass + for font_dir in font_dirs: + font = Font(os.path.join(font_path, font_dir)) + if font.name == "" or font.id == "": + continue + self.fonts[font.name] = font + self.fonts_by_id[font.id] = font if len(self.fonts) == 0: info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) @@ -165,13 +156,13 @@ class LetteringFrame(wx.Frame): self.font_chooser.Append(font.name) def get_font_names(self): - font_names = [font.name for font in self.fonts.itervalues()] + font_names = [font.name for font in self.fonts.values()] font_names.sort() return font_names def get_font_descriptions(self): - return {font.name: font.description for font in self.fonts.itervalues()} + return {font.name: font.description for font in self.fonts.values()} def set_initial_font(self, font_id): if font_id: @@ -191,7 +182,7 @@ class LetteringFrame(wx.Frame): try: return self.fonts_by_id[self.DEFAULT_FONT] except KeyError: - return self.fonts.values()[0] + return list(self.fonts.values())[0] def on_change(self, attribute, event): self.settings[attribute] = event.GetEventObject().GetValue() @@ -202,7 +193,11 @@ class LetteringFrame(wx.Frame): self.settings.font = font.id self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) - font_variants = font.has_variants() + font_variants = [] + try: + font_variants = font.has_variants() + except FontError: + pass # Update font description color = (0, 0, 0) @@ -212,7 +207,7 @@ class LetteringFrame(wx.Frame): description = _('This font has no available font variant. Please update or remove the font.') self.font_description.SetLabel(description) self.font_description.SetForegroundColour(color) - self.font_description.Wrap(self.GetSize().width - 20) + self.font_description.Wrap(self.GetSize().width - 35) if font.reversible: self.back_and_forth_checkbox.Enable() @@ -230,18 +225,24 @@ class LetteringFrame(wx.Frame): self.trim_checkbox.SetValue(False) self.update_preview() - self.GetSizer().Layout() + self.Layout() + + def resize(self, event=None): + description = self.font_description.GetLabel().replace("\n", " ") + self.font_description.SetLabel(description) + self.font_description.Wrap(self.GetSize().width - 35) + self.Layout() def update_preview(self, event=None): self.preview.update() - def update_lettering(self): + def update_lettering(self, raise_error=False): del self.group[:] if self.settings.scale == 100: destination_group = self.group else: - destination_group = inkex.etree.SubElement(self.group, SVG_GROUP_TAG, { + destination_group = etree.SubElement(self.group, SVG_GROUP_TAG, { # L10N The user has chosen to scale the text by some percentage # (50%, 200%, etc). If you need to use the percentage symbol, # make sure to double it (%%). @@ -249,7 +250,14 @@ class LetteringFrame(wx.Frame): }) font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) - font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + try: + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + except FontError as e: + if raise_error: + inkex.errormsg("Error: Text cannot be applied to the document.\n%s" % e) + return + else: + pass if self.settings.scale != 100: destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) @@ -295,7 +303,7 @@ class LetteringFrame(wx.Frame): def apply(self, event): self.preview.disable() - self.update_lettering() + self.update_lettering(True) self.save_settings() self.close() @@ -369,10 +377,10 @@ class Lettering(CommandsExtension): self.cancelled = True def get_or_create_group(self): - if self.selected: + if self.svg.selected: groups = set() - for node in self.selected.itervalues(): + for node in self.svg.selected.values(): if node.tag == SVG_GROUP_TAG and INKSTITCH_LETTERING in node.attrib: groups.add(node) @@ -391,9 +399,9 @@ class Lettering(CommandsExtension): return list(groups)[0] else: self.ensure_current_layer() - return inkex.etree.SubElement(self.current_layer, SVG_GROUP_TAG, { + return etree.SubElement(self.svg.get_current_layer(), SVG_GROUP_TAG, { INKSCAPE_LABEL: _("Ink/Stitch Lettering"), - "transform": get_correction_transform(self.current_layer, child=True) + "transform": get_correction_transform(self.svg.get_current_layer(), child=True) }) def effect(self): @@ -405,7 +413,7 @@ class Lettering(CommandsExtension): display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2)) + frame.SetPosition((int(display_size[0]), int(display_size[3] / 2 - frame_size[1] / 2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/lettering_custom_font_dir.py b/lib/extensions/lettering_custom_font_dir.py new file mode 100644 index 000000000..0103c7d61 --- /dev/null +++ b/lib/extensions/lettering_custom_font_dir.py @@ -0,0 +1,48 @@ +import json +import os + +import appdirs +from inkex import errormsg + +from ..i18n import _ +from .base import InkstitchExtension + + +class LetteringCustomFontDir(InkstitchExtension): + ''' + This extension will create a json file to store a custom directory path for additional user fonts + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-d", "--path", type=str, default="", dest="path") + + def effect(self): + path = self.options.path + if not os.path.isdir(path): + errormsg(_("Please specify the directory of your custom fonts.")) + return + + data = {'custom_font_dir': '%s' % path} + + try: + config_path = appdirs.user_config_dir('inkstitch') + except ImportError: + config_path = os.path.expanduser('~/.inkstitch') + config_path = os.path.join(config_path, 'custom_dirs.json') + + with open(config_path, 'w', encoding="utf8") as font_data: + json.dump(data, font_data, indent=4, ensure_ascii=False) + + +def get_custom_font_dir(): + custom_font_dir_path = os.path.join(appdirs.user_config_dir('inkstitch'), 'custom_dirs.json') + try: + with open(custom_font_dir_path, 'r') as custom_dirs: + custom_dir = json.load(custom_dirs) + except (IOError, ValueError): + return "" + try: + return custom_dir['custom_font_dir'] + except KeyError: + pass + return "" diff --git a/lib/extensions/lettering_generate_json.py b/lib/extensions/lettering_generate_json.py new file mode 100644 index 000000000..9b44c3670 --- /dev/null +++ b/lib/extensions/lettering_generate_json.py @@ -0,0 +1,76 @@ +import json +import os +import sys + +from inkex import Boolean + +from ..i18n import _ +from ..lettering.kerning import FontKerning +from .base import InkstitchExtension + + +class LetteringGenerateJson(InkstitchExtension): + ''' + This extension helps font creators to generate the json file for the lettering tool + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-n", "--font-name", type=str, default="Font", dest="font_name") + self.arg_parser.add_argument("-d", "--font-description", type=str, default="Description", dest="font_description") + self.arg_parser.add_argument("-s", "--auto-satin", type=Boolean, default="true", dest="auto_satin") + self.arg_parser.add_argument("-r", "--reversible", type=Boolean, default="true", dest="reversible") + self.arg_parser.add_argument("-g", "--default-glyph", type=str, default="", dest="default_glyph") + self.arg_parser.add_argument("-i", "--min-scale", type=float, default=1, dest="min_scale") + self.arg_parser.add_argument("-a", "--max-scale", type=float, default=1, dest="max_scale") + self.arg_parser.add_argument("-l", "--leading", type=int, default=0, dest="leading") + self.arg_parser.add_argument("-p", "--font-file", type=str, default="", dest="path") + + def effect(self): + # file paths + path = self.options.path + if not os.path.isfile(path): + print(_("Please specify a font file."), file=sys.stderr) + return + output_path = os.path.join(os.path.dirname(path), 'font.json') + + # kerning + kerning = FontKerning(path) + + horiz_adv_x = kerning.horiz_adv_x() + hkern = kerning.hkern() + word_spacing = kerning.word_spacing() + letter_spacing = kerning.letter_spacing() + units_per_em = kerning.units_per_em() + # missing_glyph_spacing = kerning.missing_glyph_spacing() + + # if letter spacing returns 0, it hasn't been specified in the font file + # Ink/Stitch will calculate the width of each letter automatically + if letter_spacing == 0: + letter_spacing = None + + # if leading (line height) is set to 0, the font author wants Ink/Stitch to use units_per_em + # if units_per_em is not defined in the font file a default value will be returned + if self.options.leading == 0: + leading = units_per_em + else: + leading = self.options.leading + + # collect data + data = {'name': self.options.font_name, + 'description': self.options.font_description, + 'leading': leading, + 'auto_satin': self.options.auto_satin, + 'reversible': self.options.reversible, + 'default_glyph': self.options.default_glyph, + 'min_scale': self.options.min_scale, + 'max_scale': self.options.max_scale, + 'horiz_adv_x_default': letter_spacing, + 'horiz_adv_x_space': word_spacing, + 'units_per_em': units_per_em, + 'horiz_adv_x': horiz_adv_x, + 'kerning_pairs': hkern + } + + # write data to font.json into the same directory as the font file + with open(output_path, 'w', encoding="utf8") as font_data: + json.dump(data, font_data, indent=4, ensure_ascii=False) diff --git a/lib/extensions/lettering_remove_kerning.py b/lib/extensions/lettering_remove_kerning.py new file mode 100644 index 000000000..aec8717ed --- /dev/null +++ b/lib/extensions/lettering_remove_kerning.py @@ -0,0 +1,30 @@ +import os + +from inkex import NSS +from lxml import etree + +from .base import InkstitchExtension + + +class LetteringRemoveKerning(InkstitchExtension): + ''' + This extension helps font creators to generate the json file for the lettering tool + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-p", "--font-files", type=str, default="", dest="paths") + + def effect(self): + # file paths + paths = self.options.paths.split("|") + for path in paths: + if not os.path.isfile(path): + continue + with open(path, 'r+', encoding="utf-8") as fontfile: + svg = etree.parse(fontfile) + xpath = ".//svg:font[1]" + kerning = svg.xpath(xpath, namespaces=NSS)[0] + kerning.getparent().remove(kerning) + fontfile.seek(0) + fontfile.write(etree.tostring(svg).decode('utf-8')) + fontfile.truncate() diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index d33ab2ba6..f1c2fb460 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -12,7 +12,7 @@ class ObjectCommands(CommandsExtension): if not self.get_elements(): return - if not self.selected: + if not self.svg.selected: inkex.errormsg(_("Please select one or more objects to which to attach commands.")) return diff --git a/lib/extensions/output.py b/lib/extensions/output.py index ccf4d7cba..52e9d3a9d 100644 --- a/lib/extensions/output.py +++ b/lib/extensions/output.py @@ -11,7 +11,7 @@ class Output(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - def getoptions(self, args=sys.argv[1:]): + def parse_arguments(self, args=sys.argv[1:]): # inkex's option parsing can't handle arbitrary command line arguments # that may be passed for a given output format, so we'll just parse the # args ourselves. :P @@ -39,14 +39,16 @@ class Output(InkstitchExtension): self.file_extension = self.settings.pop('format') del sys.argv[1:] - InkstitchExtension.getoptions(self, extra_args) + InkstitchExtension.parse_arguments(self, extra_args) def effect(self): if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, disable_ties=self.settings.get('laser_mode', False)) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len, disable_ties=self.settings.get('laser_mode', False)) temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.file_extension, delete=False) @@ -62,7 +64,7 @@ class Output(InkstitchExtension): # inkscape will read the file contents from stdout and copy # to the destination file that the user chose with open(temp_file.name, "rb") as output_file: - sys.stdout.write(output_file.read()) + sys.stdout.buffer.write(output_file.read()) sys.stdout.flush() # clean up the temp file diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 600a4669d..910941fe2 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -151,7 +151,7 @@ class ParamsTab(ScrolledPanel): # because they're grayed out anyway. return values - for name, input in self.param_inputs.iteritems(): + for name, input in self.param_inputs.items(): if input in self.changed_inputs and input != self.toggle_checkbox: values[name] = input.GetValue() @@ -161,7 +161,7 @@ class ParamsTab(ScrolledPanel): values = self.get_values() for node in self.nodes: # print >> sys.stderr, "apply: ", self.name, node.id, values - for name, value in values.iteritems(): + for name, value in values.items(): node.set_param(name, value) def on_change(self, callable): @@ -181,7 +181,7 @@ class ParamsTab(ScrolledPanel): def load_preset(self, preset): preset_data = preset.get(self.name, {}) - for name, value in preset_data.iteritems(): + for name, value in preset_data.items(): if name in self.param_inputs: self.param_inputs[name].SetValue(value) self.changed_inputs.add(self.param_inputs[name]) @@ -198,16 +198,16 @@ class ParamsTab(ScrolledPanel): description = _("These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += u"\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: if len(self.dependent_tabs) == 1: - description += u"\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) else: - description += u"\n • " + _("Disabling this tab will disable the following tab.") + description += "\n • " + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += u"\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -285,7 +285,7 @@ class ParamsTab(ScrolledPanel): self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) - self.inputs_to_params = {v: k for k, v in self.param_inputs.iteritems()} + self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) self.SetSizer(box) @@ -443,9 +443,9 @@ class SettingsFrame(wx.Frame): self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) - sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 5) - sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) - sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) + sizer_3.Add(self.cancel_button, 0, wx.RIGHT, 5) + sizer_3.Add(self.use_last_button, 0, wx.RIGHT | wx.BOTTOM, 5) + sizer_3.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 5) sizer_1.Add(sizer_3, 0, wx.ALIGN_RIGHT, 0) self.SetSizer(sizer_1) sizer_1.Fit(self) @@ -491,7 +491,7 @@ class Params(InkstitchExtension): element.order = z nodes_by_class[cls].append(element) - return sorted(nodes_by_class.items(), key=lambda (cls, nodes): cls.__name__) + return sorted(list(nodes_by_class.items()), key=lambda cls_nodes: cls_nodes[0].__name__) def get_values(self, param, nodes): getter = 'get_param' @@ -501,17 +501,16 @@ class Params(InkstitchExtension): else: getter = 'get_param' - values = filter(lambda item: item is not None, - (getattr(node, getter)(param.name, param.default) for node in nodes)) + values = [item for item in (getattr(node, getter)(param.name, param.default) for node in nodes) if item is not None] return values def group_params(self, params): def by_group_and_sort_index(param): - return param.group, param.sort_index + return param.group or "", param.sort_index def by_group(param): - return param.group + return param.group or "" return groupby(sorted(params, key=by_group_and_sort_index), by_group) @@ -526,7 +525,7 @@ class Params(InkstitchExtension): # If multiple tabs are enabled, make sure dependent # tabs are grouped with the parent. - parent, + parent and parent.name, # Within parent/dependents, put the parent first. tab == parent @@ -565,12 +564,13 @@ class Params(InkstitchExtension): parent_tab = None new_tabs = [] + for group, params in self.group_params(params): tab_name = group or cls.element_name tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, params=list(params), nodes=nodes) new_tabs.append(tab) - if group is None: + if group == "": parent_tab = tab self.assign_parents(new_tabs, parent_tab) @@ -594,7 +594,7 @@ class Params(InkstitchExtension): display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2)) + frame.SetPosition((int(display_size[0]), int(display_size[3]/2 - frame_size[1]/2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index 8af07cf7b..1218b5e99 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -1,19 +1,19 @@ -from copy import deepcopy -from datetime import date import errno import json import logging import os import socket import sys -from threading import Thread import time +from copy import deepcopy +from datetime import date +from threading import Thread import appdirs -from flask import Flask, request, Response, send_from_directory, jsonify -import inkex -from jinja2 import Environment, FileSystemLoader, select_autoescape import requests +from flask import Flask, Response, jsonify, request, send_from_directory +from jinja2 import Environment, FileSystemLoader, select_autoescape +from lxml import etree from ..gui import open_url from ..i18n import translation as inkstitch_translation @@ -72,6 +72,11 @@ class PrintPreviewServer(Thread): def __setup_app(self): # noqa: C901 self.__set_resources_path() + + # Disable warning about using a development server in a production environment + cli = sys.modules['flask.cli'] + cli.show_server_banner = lambda *x: None + self.app = Flask(__name__) @self.app.route('/') @@ -211,13 +216,21 @@ class Print(InkstitchExtension): layers = svg.findall("./g[@%s='layer']" % INKSCAPE_GROUPMODE) stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + # Make sure there is no leftover translation from stitch plan preview + stitch_plan_layer.pop('transform') + + # objects outside of the viewbox are invisible + # TODO: if we want them to be seen, we need to redefine document size to fit the design + # this is just a quick fix and doesn't work on realistic view + svg.set('style', 'overflow:visible;') + # First, delete all of the other layers. We don't need them and they'll # just bulk up the SVG. for layer in layers: if layer is not stitch_plan_layer: svg.remove(layer) - overview_svg = inkex.etree.tostring(svg) + overview_svg = etree.tostring(svg).decode('utf-8') color_block_groups = stitch_plan_layer.getchildren() color_block_svgs = [] @@ -229,7 +242,7 @@ class Print(InkstitchExtension): stitch_plan_layer.append(group) # save an SVG preview - color_block_svgs.append(inkex.etree.tostring(svg)) + color_block_svgs.append(etree.tostring(svg).decode('utf-8')) return overview_svg, color_block_svgs @@ -269,13 +282,15 @@ class Print(InkstitchExtension): # objects. It's almost certain they meant to print the whole design. # If they really wanted to print just a few objects, they could set # the rest invisible temporarily. - self.selected = {} + self.svg.selected.clear() if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False) diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py index 2a4d06dd7..6ccdb7037 100644 --- a/lib/extensions/remove_embroidery_settings.py +++ b/lib/extensions/remove_embroidery_settings.py @@ -1,4 +1,4 @@ -import inkex +from inkex import NSS, Boolean from ..commands import find_commands from ..svg.svg import find_elements @@ -8,9 +8,9 @@ from .base import InkstitchExtension class RemoveEmbroiderySettings(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-p", "--del_params", dest="del_params", type="inkbool", default=True) - self.OptionParser.add_option("-c", "--del_commands", dest="del_commands", type="inkbool", default=False) - self.OptionParser.add_option("-d", "--del_print", dest="del_print", type="inkbool", default=False) + self.arg_parser.add_argument("-p", "--del_params", dest="del_params", type=Boolean, default=True) + self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=Boolean, default=False) + self.arg_parser.add_argument("-d", "--del_print", dest="del_print", type=Boolean, default=False) def effect(self): self.svg = self.document.getroot() @@ -30,24 +30,24 @@ class RemoveEmbroiderySettings(InkstitchExtension): self.remove_element(print_setting) def remove_params(self): - if not self.selected: + if not self.svg.selected: xpath = ".//svg:path" elements = find_elements(self.svg, xpath) self.remove_inkstitch_attributes(elements) else: - for node in self.selected: + for node in self.svg.selected: elements = self.get_selected_elements(node) self.remove_inkstitch_attributes(elements) def remove_commands(self): - if not self.selected: + if not self.svg.selected: # we are not able to grab commands by a specific id # so let's move through every object instead and see if it has a command xpath = ".//svg:path|.//svg:circle|.//svg:rect|.//svg:ellipse" elements = find_elements(self.svg, xpath) else: elements = [] - for node in self.selected: + for node in self.svg.selected: elements.extend(self.get_selected_elements(node)) if elements: @@ -56,7 +56,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): group = command.connector.getparent() group.getparent().remove(group) - if not self.selected: + if not self.svg.selected: # remove standalone commands standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]" self.remove_elements(standalone_commands) @@ -84,5 +84,5 @@ class RemoveEmbroiderySettings(InkstitchExtension): def remove_inkstitch_attributes(self, elements): for element in elements: for attrib in element.attrib: - if attrib.startswith(inkex.NSS['inkstitch'], 1): + if attrib.startswith(NSS['inkstitch'], 1): del element.attrib[attrib] diff --git a/lib/extensions/reorder.py b/lib/extensions/reorder.py new file mode 100644 index 000000000..4db027600 --- /dev/null +++ b/lib/extensions/reorder.py @@ -0,0 +1,36 @@ +import inkex + +from .base import InkstitchExtension + + +class Reorder(InkstitchExtension): + # Remove selected objects from the document and readd them in the order they + # were selected. + + def get_selected_in_order(self): + selected = [] + + for i in self.options.ids: + path = '//*[@id="%s"]' % i + for node in self.document.xpath(path, namespaces=inkex.NSS): + selected.append(node) + + return selected + + def effect(self): + objects = self.get_selected_in_order() + + for obj in objects[1:]: + obj.getparent().remove(obj) + + insert_parent = objects[0].getparent() + insert_pos = insert_parent.index(objects[0]) + + insert_parent.remove(objects[0]) + + insert_parent[insert_pos:insert_pos] = objects + + +if __name__ == '__main__': + e = Reorder() + e.affect() diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py index b89e24a74..e1c398b58 100644 --- a/lib/extensions/stitch_plan_preview.py +++ b/lib/extensions/stitch_plan_preview.py @@ -4,7 +4,6 @@ from .base import InkstitchExtension class StitchPlanPreview(InkstitchExtension): - def effect(self): # delete old stitch plan svg = self.document.getroot() @@ -15,9 +14,13 @@ class StitchPlanPreview(InkstitchExtension): # create new stitch plan if not self.get_elements(): return + + realistic = False + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches) - render_stitch_plan(svg, stitch_plan) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) + render_stitch_plan(svg, stitch_plan, realistic) # translate stitch plan to the right side of the canvas layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") diff --git a/lib/extensions/troubleshoot.py b/lib/extensions/troubleshoot.py index 6b63390a9..bd352d8fd 100644 --- a/lib/extensions/troubleshoot.py +++ b/lib/extensions/troubleshoot.py @@ -1,12 +1,13 @@ import textwrap -import inkex +from inkex import errormsg +from lxml import etree from ..commands import add_layer_commands from ..elements.validation import (ObjectTypeWarning, ValidationError, ValidationWarning) from ..i18n import _ -from ..svg import get_correction_transform +from ..svg.path import get_correction_transform from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SODIPODI_ROLE, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_TEXT_TAG, SVG_TSPAN_TAG) @@ -43,7 +44,7 @@ class Troubleshoot(InkstitchExtension): message += "\n\n" message += _("If you are still having trouble with a shape not being embroidered, " "check if it is in a layer with an ignore command.") - inkex.errormsg(message) + errormsg(message) def insert_pointer(self, problem): correction_transform = get_correction_transform(self.troubleshoot_layer) @@ -61,7 +62,7 @@ class Troubleshoot(InkstitchExtension): pointer_style = "stroke:#000000;stroke-width:0.2;fill:%s;" % (fill_color) text_style = "fill:%s;stroke:#000000;stroke-width:0.2;font-size:8px;text-align:center;text-anchor:middle" % (fill_color) - path = inkex.etree.Element( + path = etree.Element( SVG_PATH_TAG, { "id": self.uniqueId("inkstitch__invalid_pointer__"), @@ -73,7 +74,7 @@ class Troubleshoot(InkstitchExtension): ) layer.insert(0, path) - text = inkex.etree.Element( + text = etree.Element( SVG_TEXT_TAG, { INKSCAPE_LABEL: _('Description'), @@ -85,7 +86,7 @@ class Troubleshoot(InkstitchExtension): ) layer.append(text) - tspan = inkex.etree.Element(SVG_TSPAN_TAG) + tspan = etree.Element(SVG_TSPAN_TAG) tspan.text = problem.name text.append(tspan) @@ -94,7 +95,7 @@ class Troubleshoot(InkstitchExtension): layer = svg.find(".//*[@id='__validation_layer__']") if layer is None: - layer = inkex.etree.Element( + layer = etree.Element( SVG_GROUP_TAG, { 'id': '__validation_layer__', @@ -109,7 +110,7 @@ class Troubleshoot(InkstitchExtension): add_layer_commands(layer, ["ignore_layer"]) - error_group = inkex.etree.SubElement( + error_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -118,7 +119,7 @@ class Troubleshoot(InkstitchExtension): }) layer.append(error_group) - warning_group = inkex.etree.SubElement( + warning_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -127,7 +128,7 @@ class Troubleshoot(InkstitchExtension): }) layer.append(warning_group) - type_warning_group = inkex.etree.SubElement( + type_warning_group = etree.SubElement( layer, SVG_GROUP_TAG, { @@ -145,7 +146,7 @@ class Troubleshoot(InkstitchExtension): svg = self.document.getroot() text_x = str(float(svg.get('viewBox', '0 0 800 0').split(' ')[2]) + 5.0) - text_container = inkex.etree.Element( + text_container = etree.Element( SVG_TEXT_TAG, { "x": text_x, @@ -160,7 +161,7 @@ class Troubleshoot(InkstitchExtension): ["", ""] ] - for problem_type, problems in problem_types.items(): + for problem_type, problems in list(problem_types.items()): if problem_type == "error": text_color = "#ff0000" problem_type_header = _("Errors") @@ -202,7 +203,7 @@ class Troubleshoot(InkstitchExtension): text = self.split_text(text) for text_line in text: - tspan = inkex.etree.Element( + tspan = etree.Element( SVG_TSPAN_TAG, { SODIPODI_ROLE: "line", diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index aebff3312..fedf8c655 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -1,36 +1,34 @@ import os import sys import tempfile +from copy import deepcopy from zipfile import ZipFile -import inkex +from inkex import Boolean +from lxml import etree + import pyembroidery from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import patches_to_stitch_plan -from ..svg import PIXELS_PER_MM -from .base import InkstitchExtension from ..threads import ThreadCatalog +from .base import InkstitchExtension class Zip(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self) - self.OptionParser.add_option("-c", "--collapse_len_mm", - action="store", type="float", - dest="collapse_length_mm", default=3.0, - help="max collapse length (mm)") # it's kind of obnoxious that I have to do this... self.formats = [] for format in pyembroidery.supported_formats(): if 'writer' in format and format['category'] == 'embroidery': extension = format['extension'] - self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension) + self.arg_parser.add_argument('--format-%s' % extension, type=Boolean, dest=extension) self.formats.append(extension) - self.OptionParser.add_option('--format-svg', type="inkbool", dest='svg') - self.OptionParser.add_option('--format-threadlist', type="inkbool", dest='threadlist') + self.arg_parser.add_argument('--format-svg', type=Boolean, dest='svg') + self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist') self.formats.append('svg') self.formats.append('threadlist') @@ -38,8 +36,10 @@ class Zip(InkstitchExtension): if not self.get_elements(): return + self.metadata = self.get_inkstitch_metadata() + collapse_len = self.metadata['collapse_len_mm'] patches = self.elements_to_patches(self.elements) - stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) + stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len) base_file_name = self.get_base_file_name() path = tempfile.mkdtemp() @@ -50,10 +50,10 @@ class Zip(InkstitchExtension): if getattr(self.options, format): output_file = os.path.join(path, "%s.%s" % (base_file_name, format)) if format == 'svg': - output = open(output_file, 'w') - output.write(inkex.etree.tostring(self.document.getroot())) - output.close() - if format == 'threadlist': + document = deepcopy(self.document.getroot()) + with open(output_file, 'w', encoding='utf-8') as svg: + svg.write(etree.tostring(document).decode('utf-8')) + elif format == 'threadlist': output_file = os.path.join(path, "%s_%s.txt" % (base_file_name, _("threadlist"))) output = open(output_file, 'w') output.write(self.get_threadlist(stitch_plan, base_file_name)) @@ -76,8 +76,8 @@ class Zip(InkstitchExtension): # inkscape will read the file contents from stdout and copy # to the destination file that the user chose - with open(temp_file.name) as output_file: - sys.stdout.write(output_file.read()) + with open(temp_file.name, 'rb') as output_file: + sys.stdout.buffer.write(output_file.read()) os.remove(temp_file.name) for file in files: diff --git a/lib/gui/electron.py b/lib/gui/electron.py index 83486f785..ef215fb53 100644 --- a/lib/gui/electron.py +++ b/lib/gui/electron.py @@ -27,5 +27,5 @@ def open_url(url): cwd = get_bundled_dir("electron") # Any output on stdout will crash inkscape. - null = open(os.devnull, 'w') - return subprocess.Popen(command, cwd=cwd, stdout=null) + with open(os.devnull, 'w') as null: + return subprocess.Popen(command, cwd=cwd, stdout=null) diff --git a/lib/gui/presets.py b/lib/gui/presets.py index b3312f0e0..2c0d04816 100644 --- a/lib/gui/presets.py +++ b/lib/gui/presets.py @@ -64,7 +64,7 @@ class PresetsPanel(wx.Panel): presets_sizer = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) self.preset_chooser.SetMinSize((200, -1)) - presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) + presets_sizer.Add(self.preset_chooser, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) @@ -106,7 +106,7 @@ class PresetsPanel(wx.Panel): json.dump(presets, presets_file) def update_preset_list(self): - preset_names = self._load_presets().keys() + preset_names = list(self._load_presets().keys()) preset_names = [preset for preset in preset_names if not self.is_hidden(preset)] self.preset_chooser.SetItems(sorted(preset_names)) diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index 48709cb85..996bc8f96 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -1,21 +1,16 @@ -from itertools import izip import sys -from threading import Thread, Event import time import traceback +from threading import Event, Thread import wx from wx.lib.intctrl import IntCtrl from ..i18n import _ -from ..stitch_plan import stitch_plan_from_file, patches_to_stitch_plan - +from ..stitch_plan import patches_to_stitch_plan, stitch_plan_from_file from ..svg import PIXELS_PER_MM - - from .dialogs import info_dialog - # L10N command label at bottom of simulator window COMMAND_NAMES = [_("STITCH"), _("JUMP"), _("TRIM"), _("STOP"), _("COLOR CHANGE")] @@ -132,7 +127,7 @@ class ControlPanel(wx.Panel): self.accel_entries = [] for shortcut_key in shortcut_keys: - eventId = wx.NewId() + eventId = wx.NewIdRef() self.accel_entries.append((shortcut_key[0], shortcut_key[1], eventId)) self.Bind(wx.EVT_MENU, shortcut_key[2], id=eventId) @@ -378,7 +373,7 @@ class DrawingPanel(wx.Panel): last_stitch = None start = time.time() - for pen, stitches in izip(self.pens, self.stitch_blocks): + for pen, stitches in zip(self.pens, self.stitch_blocks): canvas.SetPen(pen) if stitch + len(stitches) < self.current_stitch: stitch += len(stitches) @@ -423,7 +418,7 @@ class DrawingPanel(wx.Panel): while scale_width < 50: scale_width += one_mm - scale_width_mm = scale_width / self.zoom / PIXELS_PER_MM + scale_width_mm = int(scale_width / self.zoom / PIXELS_PER_MM) # The scale bar looks like this: # @@ -431,7 +426,7 @@ class DrawingPanel(wx.Panel): # |_____|_____| scale_lower_left_x = 20 - scale_lower_left_y = canvas_height - 20 + scale_lower_left_y = canvas_height - 30 canvas.DrawLines(((scale_lower_left_x, scale_lower_left_y - 6), (scale_lower_left_x, scale_lower_left_y), @@ -504,7 +499,7 @@ class DrawingPanel(wx.Panel): # We draw the thread with a thickness of 0.1mm. Real thread has a # thickness of ~0.4mm, but if we did that, we wouldn't be able to # see the individual stitches. - return wx.Pen(color.visible_on_white.rgb, width=int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY)) + return wx.Pen(list(map(int, color.visible_on_white.rgb)), int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY)) def parse_stitch_plan(self, stitch_plan): self.pens = [] @@ -698,12 +693,7 @@ class EmbroiderySimulator(wx.Frame): stitches_per_second=stitches_per_second) sizer.Add(self.simulator_panel, 1, wx.EXPAND) - # self.SetSizerAndFit() sets the minimum size so that the buttons don't - # get squished. But it then also shrinks the window down to that size. - self.SetSizerAndFit(sizer) - - # Therefore we have to reapply the size that the caller asked for. - self.SetSize(size) + self.SetSizeHints(sizer.CalcMin()) self.Bind(wx.EVT_CLOSE, self.on_close) diff --git a/lib/i18n.py b/lib/i18n.py index f57bbf9c7..50e71c921 100644 --- a/lib/i18n.py +++ b/lib/i18n.py @@ -1,7 +1,7 @@ import gettext import os -from os.path import dirname, realpath import sys +from os.path import dirname, realpath from .utils import cache @@ -32,7 +32,7 @@ def localize(languages=None): global translation, _ translation = gettext.translation("inkstitch", locale_dir, fallback=True) - _ = translation.ugettext + _ = translation.gettext @cache diff --git a/lib/inx/__init__.py b/lib/inx/__init__.py index 32b8bfae2..cc2b039d6 100644 --- a/lib/inx/__init__.py +++ b/lib/inx/__init__.py @@ -1 +1 @@ -from generate import generate_inx_files +from .generate import generate_inx_files diff --git a/lib/inx/about.py b/lib/inx/about.py new file mode 100755 index 000000000..6db13865a --- /dev/null +++ b/lib/inx/about.py @@ -0,0 +1,7 @@ +from .utils import build_environment, write_inx_file + + +def generate_about_inx_file(): + env = build_environment() + template = env.get_template('about.xml') + write_inx_file("about", template.render()) diff --git a/lib/inx/extensions.py b/lib/inx/extensions.py index 030e8aa60..379e98cd5 100755 --- a/lib/inx/extensions.py +++ b/lib/inx/extensions.py @@ -1,10 +1,11 @@ import pyembroidery -from .utils import build_environment, write_inx_file -from .outputs import pyembroidery_output_formats -from ..extensions import extensions, Input, Output -from ..commands import LAYER_COMMANDS, OBJECT_COMMANDS, GLOBAL_COMMANDS, COMMANDS +from ..commands import (COMMANDS, GLOBAL_COMMANDS, LAYER_COMMANDS, + OBJECT_COMMANDS) +from ..extensions import Input, Output, extensions from ..threads import ThreadCatalog +from .outputs import pyembroidery_output_formats +from .utils import build_environment, write_inx_file def layer_commands(): @@ -41,7 +42,7 @@ def generate_extension_inx_files(): continue name = extension.name() - template = env.get_template('%s.inx' % name) + template = env.get_template('%s.xml' % name) write_inx_file(name, template.render(formats=pyembroidery_output_formats(), debug_formats=pyembroidery_debug_formats(), threadcatalog=threadcatalog(), diff --git a/lib/inx/generate.py b/lib/inx/generate.py index 941596ded..8a5b95696 100644 --- a/lib/inx/generate.py +++ b/lib/inx/generate.py @@ -1,6 +1,7 @@ +from .info import generate_info_inx_files +from .extensions import generate_extension_inx_files from .inputs import generate_input_inx_files from .outputs import generate_output_inx_files -from .extensions import generate_extension_inx_files from .utils import iterate_inx_locales @@ -9,3 +10,4 @@ def generate_inx_files(): generate_input_inx_files() generate_output_inx_files() generate_extension_inx_files() + generate_info_inx_files() diff --git a/lib/inx/info.py b/lib/inx/info.py new file mode 100755 index 000000000..f391b546f --- /dev/null +++ b/lib/inx/info.py @@ -0,0 +1,9 @@ +from .utils import build_environment, write_inx_file + + +def generate_info_inx_files(): + env = build_environment() + info_inx_files = ['about', 'embroider'] + for info in info_inx_files: + template = env.get_template('%s.xml' % info) + write_inx_file(info, template.render()) diff --git a/lib/inx/inputs.py b/lib/inx/inputs.py index d40ffeaf8..b50ec9f9d 100755 --- a/lib/inx/inputs.py +++ b/lib/inx/inputs.py @@ -11,7 +11,7 @@ def pyembroidery_input_formats(): def generate_input_inx_files(): env = build_environment() - template = env.get_template('input.inx') + template = env.get_template('input.xml') for format, description in pyembroidery_input_formats(): name = "input_%s" % format.upper() diff --git a/lib/inx/outputs.py b/lib/inx/outputs.py index aef0c8b52..ccb323c70 100644 --- a/lib/inx/outputs.py +++ b/lib/inx/outputs.py @@ -5,14 +5,17 @@ from .utils import build_environment, write_inx_file def pyembroidery_output_formats(): for format in pyembroidery.supported_formats(): - if 'writer' in format and format['category'] == 'embroidery': - yield format['extension'], format['description'] + if 'writer' in format: + description = format['description'] + if format['category'] != "embroidery": + description = "%s [DEBUG]" % description + yield format['extension'], description, format['mimetype'], format['category'] def generate_output_inx_files(): env = build_environment() - template = env.get_template('output.inx') + template = env.get_template('output.xml') - for format, description in pyembroidery_output_formats(): + for format, description, mimetype, category in pyembroidery_output_formats(): name = "output_%s" % format.upper() - write_inx_file(name, template.render(format=format, description=description)) + write_inx_file(name, template.render(format=format, mimetype=mimetype, description=description)) diff --git a/lib/inx/utils.py b/lib/inx/utils.py index a7c98a600..2fb6b21b4 100644 --- a/lib/inx/utils.py +++ b/lib/inx/utils.py @@ -6,11 +6,13 @@ from os.path import dirname from jinja2 import Environment, FileSystemLoader -from ..i18n import N_, locale_dir, translation as default_translation +from ..i18n import N_, locale_dir +from ..i18n import translation as default_translation _top_path = dirname(dirname(dirname(os.path.realpath(__file__)))) inx_path = os.path.join(_top_path, "inx") template_path = os.path.join(_top_path, "templates") +version_path = _top_path current_translation = default_translation current_locale = "en_US" @@ -26,16 +28,29 @@ def build_environment(): env.install_gettext_translations(current_translation) env.globals["locale"] = current_locale + with open(os.path.join(version_path, 'LICENSE'), 'r') as license: + env.globals["inkstitch_license"] = "".join(license.readlines()) + if "BUILD" in os.environ: # building a ZIP release, with inkstitch packaged as a binary + # About extension: add version information + with open(os.path.join(version_path, 'VERSION'), 'r') as version: + env.globals["inkstitch_version"] = "%s %s" % (version.readline(), current_locale) + # Command tag and icons path if sys.platform == "win32": - env.globals["command_tag"] = 'inkstitch/bin/inkstitch.exe' + env.globals["command_tag"] = 'inkstitch/bin/inkstitch.exe' + env.globals["image_path"] = 'inkstitch/bin/icons/' + elif sys.platform == "darwin": + env.globals["command_tag"] = 'inkstitch.app/Contents/MacOS/inkstitch' + env.globals["image_path"] = 'inkstitch.app/Contents/MacOS/icons/' else: - env.globals["command_tag"] = 'inkstitch/bin/inkstitch' + env.globals["command_tag"] = 'inkstitch/bin/inkstitch' + env.globals["image_path"] = 'inkstitch/bin/icons/' else: # user is running inkstitch.py directly as a developer - env.globals["command_tag"] = 'inkstitch.py' - + env.globals["command_tag"] = '../../inkstitch.py' + env.globals["image_path"] = '../../icons/' + env.globals["inkstitch_version"] = "Manual Install" return env @@ -49,8 +64,8 @@ def write_inx_file(name, contents): raise inx_file_name = "inkstitch_%s.inx" % name - with open(os.path.join(inx_locale_dir, inx_file_name), 'w') as inx_file: - print >> inx_file, contents.encode("utf-8") + with open(os.path.join(inx_locale_dir, inx_file_name), 'w', encoding="utf-8") as inx_file: + print(contents, file=inx_file) def iterate_inx_locales(): @@ -64,7 +79,7 @@ def iterate_inx_locales(): # generate menu items for this language in Inkscape's "Extensions" # menu. magic_string = N_("Generate INX files") - translated_magic_string = translation.ugettext(magic_string) + translated_magic_string = translation.gettext(magic_string) if translated_magic_string != magic_string or locale == "en_US": current_translation = translation diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py index 5d20d6837..5a9d345c5 100644 --- a/lib/lettering/__init__.py +++ b/lib/lettering/__init__.py @@ -1 +1 @@ -from font import Font, FontError \ No newline at end of file +from .font import Font, FontError diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 0974a1cf9..3ef99d47e 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -1,10 +1,9 @@ -# -*- coding: UTF-8 -*- - import json import os from copy import deepcopy -import inkex +from inkex import styles +from lxml import etree from ..elements import nodes_to_elements from ..exceptions import InkstitchException @@ -80,14 +79,14 @@ class Font(object): def _load_metadata(self): try: - with open(os.path.join(self.path, "font.json")) as metadata_file: + with open(os.path.join(self.path, "font.json"), encoding="utf-8") as metadata_file: self.metadata = json.load(metadata_file) except IOError: pass def _load_license(self): try: - with open(os.path.join(self.path, "LICENSE")) as license_file: + with open(os.path.join(self.path, "LICENSE"), encoding="utf-8") as license_file: self.license = license_file.read() except IOError: pass @@ -101,13 +100,9 @@ class Font(object): # we'll deal with missing variants when we apply lettering pass - def _check_variants(self): - if self.variants.get(self.default_variant) is None: - raise FontError("font not found or has no default variant") - name = localized_font_metadata('name', '') description = localized_font_metadata('description', '') - default_glyph = font_metadata('default_glyph', u"�") + default_glyph = font_metadata('defalt_glyph', "�") leading = font_metadata('leading', 5, multiplier=PIXELS_PER_MM) kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) @@ -149,10 +144,13 @@ class Font(object): return None def has_variants(self): + # returns available variants font_variants = [] for variant in FontVariant.VARIANT_TYPES: if os.path.isfile(os.path.join(self.path, "%s.svg" % variant)): font_variants.append(variant) + if not font_variants: + raise FontError(_("The font '%s' has no variants.") % self.name) return font_variants def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): @@ -182,12 +180,23 @@ class Font(object): if self.auto_satin and len(destination_group) > 0: self._apply_auto_satin(destination_group, trim) - else: - # set stroke width because it is almost invisible otherwise (why?) - for element in destination_group.iterdescendants(SVG_PATH_TAG): - style = ['stroke-width:1px' if s.startswith('stroke-width') else s for s in element.get('style').split(';')] - style = ';'.join(style) - element.set('style', '%s' % style) + + # make sure font stroke styles have always a similar look + for element in destination_group.iterdescendants(SVG_PATH_TAG): + dash_array = "" + stroke_width = "" + style = styles.Style(element.get('style')) + + if style.get('fill') == 'none': + stroke_width = ";stroke-width:1px" + if style.get('stroke-width'): + style.pop('stroke-width') + + if style.get('stroke-dasharray') and style.get('stroke-dasharray') != 'none': + stroke_width = ";stroke-width:0.5px" + dash_array = ";stroke-dasharray:3, 1" + + element.set('style', '%s%s%s' % (style.to_str(), stroke_width, dash_array)) return destination_group @@ -209,7 +218,8 @@ class Font(object): Returns: An svg:g element containing the rendered text. """ - group = inkex.etree.Element(SVG_GROUP_TAG, { + + group = etree.Element(SVG_GROUP_TAG, { INKSCAPE_LABEL: line }) diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py index 7c9fa1c0a..2071b2cb0 100644 --- a/lib/lettering/font_variant.py +++ b/lib/lettering/font_variant.py @@ -1,9 +1,7 @@ -# -*- coding: UTF-8 -*- - import os import inkex -import simplestyle +from lxml import etree from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL from .glyph import Glyph @@ -26,10 +24,10 @@ class FontVariant(object): # We use unicode characters rather than English strings for font file names # in order to be more approachable for languages other than English. - LEFT_TO_RIGHT = u"→" - RIGHT_TO_LEFT = u"←" - TOP_TO_BOTTOM = u"↓" - BOTTOM_TO_TOP = u"↑" + LEFT_TO_RIGHT = "→" + RIGHT_TO_LEFT = "←" + TOP_TO_BOTTOM = "↓" + BOTTOM_TO_TOP = "↑" VARIANT_TYPES = (LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP) @classmethod @@ -57,9 +55,9 @@ class FontVariant(object): self._load_glyphs() def _load_glyphs(self): - svg_path = os.path.join(self.path, u"%s.svg" % self.variant) - with open(svg_path) as svg_file: - svg = inkex.etree.parse(svg_file) + svg_path = os.path.join(self.path, "%s.svg" % self.variant) + with open(svg_path, encoding="utf-8") as svg_file: + svg = etree.parse(svg_file) glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS) for layer in glyph_layers: @@ -78,9 +76,9 @@ class FontVariant(object): if style_text: # The layer may be marked invisible, so we'll clear the 'display' # style. - style = simplestyle.parseStyle(group.get('style')) + style = dict(inkex.Style.parse_str(group.get('style'))) style.pop('display') - group.set('style', simplestyle.formatStyle(style)) + group.set('style', str(inkex.Style(style))) def __getitem__(self, character): if character in self.glyphs: diff --git a/lib/lettering/glyph.py b/lib/lettering/glyph.py index 061a930c1..84517474e 100644 --- a/lib/lettering/glyph.py +++ b/lib/lettering/glyph.py @@ -1,9 +1,9 @@ from copy import copy -import cubicsuperpath -import simpletransform +from inkex import paths, transforms -from ..svg import apply_transforms, get_guides +from ..svg import get_guides +from ..svg.path import get_correction_transform from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG @@ -37,8 +37,9 @@ class Glyph(object): def _process_group(self, group): new_group = copy(group) - new_group.attrib.pop('transform', None) - del new_group[:] # delete references to the original group's children + # new_group.attrib.pop('transform', None) + # delete references to the original group's children + del new_group[:] for node in group: if node.tag == SVG_GROUP_TAG: @@ -47,11 +48,9 @@ class Glyph(object): node_copy = copy(node) if "d" in node.attrib: - # Convert the path to absolute coordinates, incorporating all - # nested transforms. - path = cubicsuperpath.parsePath(node.get("d")) - apply_transforms(path, node) - node_copy.set("d", cubicsuperpath.formatPath(path)) + transform = -transforms.Transform(get_correction_transform(node, True)) + path = paths.Path(node.get("d")).transform(transform).to_absolute() + node_copy.set("d", str(path)) # Delete transforms from paths and groups, since we applied # them to the paths already. @@ -71,16 +70,18 @@ class Glyph(object): self.baseline = 0 def _process_bbox(self): - left, right, top, bottom = simpletransform.computeBBox(self.node.iterdescendants()) - + bbox = [paths.Path(node.get("d")).bounding_box() for node in self.node.iterdescendants(SVG_PATH_TAG)] + left, right = min([box.left for box in bbox]), max([box.right for box in bbox]) self.width = right - left self.min_x = left def _move_to_origin(self): translate_x = -self.min_x translate_y = -self.baseline - transform = "translate(%s, %s)" % (translate_x, translate_y) + transform = transforms.Transform("translate(%s, %s)" % (translate_x, translate_y)) for node in self.node.iter(SVG_PATH_TAG): - node.set('transform', transform) - simpletransform.fuseTransform(node) + path = paths.Path(node.get("d")) + path = path.transform(transform) + node.set('d', str(path)) + node.attrib.pop('transform', None) diff --git a/lib/lettering/kerning.py b/lib/lettering/kerning.py new file mode 100644 index 000000000..920e7d59f --- /dev/null +++ b/lib/lettering/kerning.py @@ -0,0 +1,69 @@ +from inkex import NSS +from lxml import etree + + +class FontKerning(object): + """ + This class reads kerning information from an SVG file + """ + def __init__(self, path): + with open(path) as svg: + self.svg = etree.parse(svg) + + # horiz_adv_x defines the wdith of specific letters (distance to next letter) + def horiz_adv_x(self): + # In XPath 2.0 we could use ".//svg:glyph/(@unicode|@horiz-adv-x)" + xpath = ".//svg:glyph[@unicode and @horiz-adv-x]/@*[name()='unicode' or name()='horiz-adv-x']" + hax = self.svg.xpath(xpath, namespaces=NSS) + if len(hax) == 0: + return {} + return dict(zip(hax[0::2], [int(x) for x in hax[1::2]])) + + # kerning (specific distances of two specified letters) + def hkern(self): + xpath = ".//svg:hkern[(@u1 or @g1) and (@u1 or @g1) and @k]/@*[contains(name(), '1') or contains(name(), '2') or name()='k']" + hkern = self.svg.xpath(xpath, namespaces=NSS) + for index, glyph in enumerate(hkern): + # fontTools.agl will import fontTools.misc.py23 which will output a deprecation warning + # ignore the warning for now - until the library fixed it + if index == 0: + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from fontTools.agl import toUnicode + if len(glyph) > 1 and not (index + 1) % 3 == 0: + glyph_names = glyph.split(",") + # the glyph name is written in various languages, second is english. Let's look it up. + if len(glyph_names) == 1: + hkern[index] = toUnicode(glyph) + else: + hkern[index] = toUnicode(glyph_names[1]) + k = [int(x) for x in hkern[2::3]] + u = [k + v for k, v in zip(hkern[0::3], hkern[1::3])] + hkern = dict(zip(u, k)) + return hkern + + # the space character + def word_spacing(self): + xpath = "string(.//svg:glyph[@glyph-name='space'][1]/@*[name()='horiz-adv-x'])" + word_spacing = self.svg.xpath(xpath, namespaces=NSS) or 26 + return int(word_spacing) + + # default letter spacing + def letter_spacing(self): + xpath = "string(.//svg:font[@horiz-adv-x][1]/@*[name()='horiz-adv-x'])" + letter_spacing = self.svg.xpath(xpath, namespaces=NSS) or 0 + return int(letter_spacing) + + # this value will be saved into the json file to preserve it for later font edits + # additionally it serves to automatically define the line height (leading) + def units_per_em(self, default=100): + xpath = "string(.//svg:font-face[@units-per-em][1]/@*[name()='units-per-em'])" + units_per_em = self.svg.xpath(xpath, namespaces=NSS) or default + return int(units_per_em) + + """ + def missing_glyph_spacing(self): + xpath = "string(.//svg:missing-glyph/@*[name()='horiz-adv-x'])" + return float(self.svg.xpath(xpath, namespaces=NSS)) + """ diff --git a/lib/output.py b/lib/output.py index fbcdea6c3..60579801b 100644 --- a/lib/output.py +++ b/lib/output.py @@ -1,4 +1,5 @@ import sys +import inkex import pyembroidery @@ -27,7 +28,8 @@ def _string_to_floats(string): return [float(num) for num in floats] -def get_origin(svg, (minx, miny, maxx, maxy)): +def get_origin(svg, xxx_todo_changeme): + (minx, miny, maxx, maxy) = xxx_todo_changeme origin_command = global_command(svg, "origin") if origin_command: @@ -91,5 +93,6 @@ def write_embroidery_file(file_path, stitch_plan, svg, settings={}): except IOError as e: # L10N low-level file error. %(error)s is (hopefully?) translated by # the user's system automatically. - print >> sys.stderr, _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.strerror) + msg = _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.strerror) + inkex.errormsg(msg) sys.exit(1) diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 0b2f75cd1..de66cb10a 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -5,7 +5,8 @@ from .stitch import Stitch from .ties import add_ties -def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM, disable_ties=False): +def patches_to_stitch_plan(patches, collapse_len=None, disable_ties=False): + """Convert a collection of inkstitch.element.Patch objects to a StitchPlan. * applies instructions embedded in the Patch such as trim_after and stop_after @@ -13,6 +14,7 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM, disable_ti * adds jump-stitches between patches if necessary """ + collapse_len = (collapse_len or 3.0) * PIXELS_PER_MM stitch_plan = StitchPlan() color_block = stitch_plan.new_color_block(color=patches[0].color) diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py index e052d1443..12c636a63 100644 --- a/lib/stitches/__init__.py +++ b/lib/stitches/__init__.py @@ -1,6 +1,6 @@ -from running_stitch import * -from auto_fill import auto_fill -from fill import legacy_fill +from .auto_fill import auto_fill +from .fill import legacy_fill +from .running_stitch import * # Can't put this here because we get a circular import :( #from auto_satin import auto_satin diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index b5157a5a1..485f51e58 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -8,11 +8,12 @@ from shapely import geometry as shgeo from shapely.ops import snap from shapely.strtree import STRtree -from .fill import intersect_region_with_grating, stitch_row -from .running_stitch import running_stitch from ..debug import debug from ..svg import PIXELS_PER_MM -from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list +from ..utils.geometry import Point as InkstitchPoint +from ..utils.geometry import line_string_to_point_list +from .fill import intersect_region_with_grating, stitch_row +from .running_stitch import running_stitch class PathEdge(object): @@ -82,7 +83,7 @@ def which_outline(shape, coords): point = shgeo.Point(*coords) outlines = list(shape.boundary) - outline_indices = range(len(outlines)) + outline_indices = list(range(len(outlines))) closest = min(outline_indices, key=lambda index: outlines[index].distance(point)) return closest @@ -175,7 +176,7 @@ def insert_node(graph, shape, point): if key == "outline": edges.append(((start, end), data)) - edge, data = min(edges, key=lambda (edge, data): shgeo.LineString(edge).distance(projected_point)) + edge, data = min(edges, key=lambda edge_data: shgeo.LineString(edge_data[0]).distance(projected_point)) graph.remove_edge(*edge, key="outline") graph.add_edge(edge[0], node, key="outline", **data) @@ -204,7 +205,7 @@ def add_boundary_travel_nodes(graph, shape): if length > 1: # Just plot a point every pixel, that should be plenty of # resolution. A pixel is around a quarter of a millimeter. - for i in xrange(1, int(length)): + for i in range(1, int(length)): subpoint = segment.interpolate(i) graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index) @@ -480,7 +481,7 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None graph = graph.copy() if not starting_point: - starting_point = graph.nodes.keys()[0] + starting_point = list(graph.nodes.keys())[0] starting_node = nearest_node(graph, starting_point) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index 112c714b9..5ce91a495 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -1,14 +1,12 @@ import math -from itertools import chain, izip +from itertools import chain +import inkex import networkx as nx +from lxml import etree from shapely import geometry as shgeo from shapely.geometry import Point as ShapelyPoint -import cubicsuperpath -import inkex -import simplestyle - from ..commands import add_commands from ..elements import SatinColumn, Stroke from ..i18n import _ @@ -87,7 +85,7 @@ class SatinSegment(object): num_segments = int(math.ceil(self.center_line.length / segment_size)) segments = [] - for i in xrange(num_segments): + for i in range(num_segments): segments.append(SatinSegment(self.satin, float(i) / num_segments, float(i + 1) / num_segments, @@ -216,13 +214,13 @@ class RunningStitch(object): original_element.node.get(INKSTITCH_ATTRIBS['contour_underlay_stitch_length_mm'], '') def to_element(self): - node = inkex.etree.Element(SVG_PATH_TAG) - node.set("d", cubicsuperpath.formatPath( - line_strings_to_csp([self.path]))) + node = etree.Element(SVG_PATH_TAG) + d = str(inkex.paths.CubicSuperPath(line_strings_to_csp([self.path]))) + node.set("d", d) style = self.original_element.parse_style() style['stroke-dasharray'] = "0.5,0.5" - style = simplestyle.formatStyle(style) + style = str(inkex.Style(style)) node.set("style", style) node.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.running_stitch_length) @@ -445,8 +443,9 @@ def add_jumps(graph, elements, preserve_order): point2 = graph.nodes[node2]['point'] potential_edges.append((point1, point2)) - edge = min(potential_edges, key=lambda (p1, p2): p1.distance(p2)) - graph.add_edge(str(edge[0]), str(edge[1]), jump=True) + if potential_edges: + edge = min(potential_edges, key=lambda p1_p2: p1_p2[0].distance(p1_p2[1])) + graph.add_edge(str(edge[0]), str(edge[1]), jump=True) else: # networkx makes this super-easy! k_edge_agumentation tells us what edges # we need to add to ensure that the graph is fully connected. We give it a @@ -497,7 +496,7 @@ def find_path(graph, starting_node, ending_node): # forth on each satin twice due to the satin edges being in the graph # twice (forward and reverse). graph = nx.Graph(graph) - graph.remove_edges_from(zip(path[:-1], path[1:])) + graph.remove_edges_from(list(zip(path[:-1], path[1:]))) final_path = [] prev = None @@ -643,14 +642,14 @@ def preserve_original_groups(elements, original_parent_nodes): to the group that contained the original SatinColumn that spawned it. """ - for element, parent in izip(elements, original_parent_nodes): + for element, parent in zip(elements, original_parent_nodes): if parent is not None: parent.append(element.node) element.node.set('transform', get_correction_transform(parent, child=True)) def create_new_group(parent, insert_index): - group = inkex.etree.Element(SVG_GROUP_TAG, { + group = etree.Element(SVG_GROUP_TAG, { INKSCAPE_LABEL: _("Auto-Satin"), "transform": get_correction_transform(parent, child=True) }) diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 924f64f6d..a19e080bb 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -3,7 +3,8 @@ import math import shapely from ..svg import PIXELS_PER_MM -from ..utils import cache, Point as InkstitchPoint +from ..utils import Point as InkstitchPoint +from ..utils import cache def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): @@ -223,7 +224,7 @@ def pull_runs(rows, shape, row_spacing): run = [] prev = None - for row_num in xrange(len(rows)): + for row_num in range(len(rows)): row = rows[row_num] first, rest = row[0], row[1:] diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index 0bb8fc7d7..57a038658 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -92,7 +92,7 @@ def bean_stitch(stitches, repeats): for stitch in stitches: new_stitches.append(stitch) - for i in xrange(repeats): + for i in range(repeats): new_stitches.extend(copy(new_stitches[-2:])) return new_stitches diff --git a/lib/svg/guides.py b/lib/svg/guides.py index 3e26a90d0..255b3e6a9 100644 --- a/lib/svg/guides.py +++ b/lib/svg/guides.py @@ -1,7 +1,7 @@ -import simpletransform +from inkex import transforms -from ..utils import string_to_floats, Point, cache -from .tags import SODIPODI_NAMEDVIEW, SODIPODI_GUIDE, INKSCAPE_LABEL +from ..utils import Point, cache, string_to_floats +from .tags import INKSCAPE_LABEL, SODIPODI_GUIDE, SODIPODI_NAMEDVIEW from .units import get_doc_size, get_viewbox_transform @@ -19,7 +19,7 @@ class InkscapeGuide(object): # convert the size from viewbox-relative to real-world pixels viewbox_transform = get_viewbox_transform(self.svg) - simpletransform.applyTransformToPoint(simpletransform.invertTransform(viewbox_transform), doc_size) + viewbox_transform = transforms.Transform(-transforms.Transform(viewbox_transform)).apply_to_point(doc_size) self.position = Point(*string_to_floats(self.node.get('position'))) diff --git a/lib/svg/path.py b/lib/svg/path.py index cc4b8cbb2..baa93443e 100644 --- a/lib/svg/path.py +++ b/lib/svg/path.py @@ -1,8 +1,7 @@ -import cubicsuperpath import inkex -import simpletransform -from tags import SVG_GROUP_TAG, SVG_LINK_TAG +from lxml import etree +from .tags import SVG_GROUP_TAG, SVG_LINK_TAG from .units import get_viewbox_transform @@ -10,7 +9,7 @@ def apply_transforms(path, node): transform = get_node_transform(node) # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) + path = path.transform(transform) return path @@ -21,7 +20,7 @@ def compose_parent_transforms(node, mat): trans = node.get('transform') if trans: - mat = simpletransform.composeTransform(simpletransform.parseTransform(trans), mat) + mat = inkex.transforms.Transform(trans) * mat if node.getparent() is not None: if node.getparent().tag in [SVG_GROUP_TAG, SVG_LINK_TAG]: mat = compose_parent_transforms(node.getparent(), mat) @@ -29,20 +28,22 @@ def compose_parent_transforms(node, mat): def get_node_transform(node): + """ + if getattr(node, "composed_transform", None): + return node.composed_transform() + """ + # start with the identity transform - transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + transform = inkex.transforms.Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) # this if is because sometimes inkscape likes to create paths outside of a layer?! if node.getparent() is not None: # combine this node's transform with all parent groups' transforms transform = compose_parent_transforms(node, transform) - if node.get('id', '').startswith('clone_'): - transform = simpletransform.parseTransform(node.get('transform', '')) - # add in the transform implied by the viewBox viewbox_transform = get_viewbox_transform(node.getroottree().getroot()) - transform = simpletransform.composeTransform(viewbox_transform, transform) + transform = viewbox_transform * transform return transform @@ -63,9 +64,9 @@ def get_correction_transform(node, child=False): # now invert it, so that we can position our objects in absolute # coordinates - transform = simpletransform.invertTransform(transform) + transform = -transform - return simpletransform.formatTransform(transform) + return str(transform) def line_strings_to_csp(line_strings): @@ -90,6 +91,6 @@ def point_lists_to_csp(point_lists): def line_strings_to_path(line_strings): csp = line_strings_to_csp(line_strings) - return inkex.etree.Element("path", { - "d": cubicsuperpath.formatPath(csp) + return etree.Element("path", { + "d": str(inkex.paths.CubicSuperPath(csp)) }) diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py index 5860ceefc..ced7d4f16 100644 --- a/lib/svg/rendering.py +++ b/lib/svg/rendering.py @@ -1,9 +1,8 @@ import math import inkex -import simplepath -import simplestyle -import simpletransform +from lxml import etree +from math import pi from ..i18n import _ from ..utils import Point, cache @@ -116,24 +115,25 @@ def realistic_stitch(start, end): start = Point(*start) stitch_length = (end - start).length() - stitch_center = (end + start) / 2.0 + stitch_center = Point((end.x+start.x)/2.0, (end[1]+start[1])/2.0) stitch_direction = (end - start) - stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) + stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) * (180 / pi) stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) # create the path by filling in the length in the template - path = simplepath.parsePath(stitch_path % stitch_length) + path = inkex.Path(stitch_path % stitch_length).to_arrays() # rotate the path to match the stitch rotation_center_x = -stitch_length / 2.0 rotation_center_y = stitch_height / 2.0 - simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) + + path = inkex.Path(path).rotate(stitch_angle, (rotation_center_x, rotation_center_y)) # move the path to the location of the stitch - simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) + path = inkex.Path(path).translate(stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) - return simplepath.formatPath(path) + return str(path) def color_block_to_point_lists(color_block): @@ -159,10 +159,9 @@ def get_correction_transform(svg): transform = get_viewbox_transform(svg) # we need to correct for the viewbox - transform = simpletransform.invertTransform(transform) - transform = simpletransform.formatTransform(transform) + transform = -inkex.transforms.Transform(transform) - return transform + return str(transform) def color_block_to_realistic_stitches(color_block, svg, destination): @@ -170,12 +169,8 @@ def color_block_to_realistic_stitches(color_block, svg, destination): color = color_block.color.visible_on_white.darker.to_hex_str() start = point_list[0] for point in point_list[1:]: - destination.append(inkex.etree.Element(SVG_PATH_TAG, { - 'style': simplestyle.formatStyle({ - 'fill': color, - 'stroke': 'none', - 'filter': 'url(#realistic-stitch-filter)' - }), + destination.append(etree.Element(SVG_PATH_TAG, { + 'style': "fill: %s; stroke: none; filter: url(#realistic-stitch-filter);" % color, 'd': realistic_stitch(start, point), 'transform': get_correction_transform(svg) })) @@ -200,12 +195,8 @@ def color_block_to_paths(color_block, svg, destination, visual_commands): color = color_block.color.visible_on_white.to_hex_str() - path = inkex.etree.Element(SVG_PATH_TAG, { - 'style': simplestyle.formatStyle({ - 'stroke': color, - 'stroke-width': "0.4", - 'fill': 'none' - }), + path = etree.Element(SVG_PATH_TAG, { + 'style': "stroke: %s; stroke-width: 0.4; fill: none;" % color, 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), 'transform': get_correction_transform(svg), INKSTITCH_ATTRIBS['manual_stitch']: 'true' @@ -223,10 +214,10 @@ def color_block_to_paths(color_block, svg, destination, visual_commands): def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True): layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") if layer is None: - layer = inkex.etree.Element(SVG_GROUP_TAG, - {'id': '__inkstitch_stitch_plan__', - INKSCAPE_LABEL: _('Stitch Plan'), - INKSCAPE_GROUPMODE: 'layer'}) + layer = etree.Element(SVG_GROUP_TAG, + {'id': '__inkstitch_stitch_plan__', + INKSCAPE_LABEL: _('Stitch Plan'), + INKSCAPE_GROUPMODE: 'layer'}) else: # delete old stitch plan del layer[:] @@ -237,10 +228,10 @@ def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True): svg.append(layer) for i, color_block in enumerate(stitch_plan): - group = inkex.etree.SubElement(layer, - SVG_GROUP_TAG, - {'id': '__color_block_%d__' % i, - INKSCAPE_LABEL: "color block %d" % (i + 1)}) + group = etree.SubElement(layer, + SVG_GROUP_TAG, + {'id': '__color_block_%d__' % i, + INKSCAPE_LABEL: "color block %d" % (i + 1)}) if realistic: color_block_to_realistic_stitches(color_block, svg, group) else: @@ -250,6 +241,6 @@ def render_stitch_plan(svg, stitch_plan, realistic=False, visual_commands=True): defs = svg.find(SVG_DEFS_TAG) if defs is None: - defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) + defs = etree.SubElement(svg, SVG_DEFS_TAG) - defs.append(inkex.etree.fromstring(realistic_filter)) + defs.append(etree.fromstring(realistic_filter)) diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 3cf7f0173..1a6b10e8c 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,4 +1,5 @@ -from inkex import NSS, etree +from inkex import NSS +from lxml import etree from ..utils import cache diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 810924a64..6fa47aa0d 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -1,10 +1,9 @@ import inkex +from lxml import etree - -# This is used below and added to the document in ../extensions/base.py. +etree.register_namespace("inkstitch", "http://inkstitch.org/namespace") inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' - SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') SVG_RECT_TAG = inkex.addNS('rect', 'svg') diff --git a/lib/svg/units.py b/lib/svg/units.py index 319e018b8..6f16d7fbc 100644 --- a/lib/svg/units.py +++ b/lib/svg/units.py @@ -1,4 +1,4 @@ -import simpletransform +import inkex from ..i18n import _ from ..utils import cache @@ -6,10 +6,14 @@ from ..utils import cache # modern versions of Inkscape use 96 pixels per inch as per the CSS standard PIXELS_PER_MM = 96 / 25.4 -# cribbed from inkscape-silhouette - def parse_length_with_units(str): + value, unit = inkex.units.parse_unit(str) + if not unit: + raise ValueError(_("parseLengthWithUnits: unknown unit %s") % str) + return value, unit + + """ ''' Parse an SVG value which may or may not have units attached This version is greatly simplified in that it only allows: no units, @@ -18,6 +22,8 @@ def parse_length_with_units(str): generality is ever needed. ''' + # cribbed from inkscape-silhouette + u = 'px' s = str.strip() if s[-2:] == 'px': @@ -46,11 +52,15 @@ def parse_length_with_units(str): raise ValueError(_("parseLengthWithUnits: unknown unit %s") % s) return v, u + """ def convert_length(length): value, units = parse_length_with_units(length) + return inkex.units.convert_unit(str(value) + units, 'px') + + """ if not units or units == "px": return value @@ -67,7 +77,7 @@ def convert_length(length): units = 'mm' if units == 'mm': - value = value / 25.4 + value /= 25.4 units = 'in' if units == 'in': @@ -76,6 +86,7 @@ def convert_length(length): return value * 96 raise ValueError(_("Unknown unit: %s") % units) + """ @cache @@ -121,7 +132,7 @@ def get_viewbox_transform(node): dx = -float(viewbox[0]) dy = -float(viewbox[1]) - transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy)) + transform = inkex.transforms.Transform("translate(%f, %f)" % (dx, dy)) try: sx = doc_width / float(viewbox[2]) @@ -132,8 +143,8 @@ def get_viewbox_transform(node): if aspect_ratio != 'none': sx = sy = max(sx, sy) if 'slice' in aspect_ratio else min(sx, sy) - scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy)) - transform = simpletransform.composeTransform(transform, scale_transform) + scale_transform = inkex.transforms.Transform("scale(%f, %f)" % (sx, sy)) + transform = transform * scale_transform except ZeroDivisionError: pass diff --git a/lib/threads/__init__.py b/lib/threads/__init__.py index 03cd777b6..1081ea140 100644 --- a/lib/threads/__init__.py +++ b/lib/threads/__init__.py @@ -1,3 +1,3 @@ -from color import ThreadColor -from palette import ThreadPalette -from catalog import ThreadCatalog +from .catalog import ThreadCatalog +from .color import ThreadColor +from .palette import ThreadPalette diff --git a/lib/threads/catalog.py b/lib/threads/catalog.py index aba2696d0..c91ce2276 100644 --- a/lib/threads/catalog.py +++ b/lib/threads/catalog.py @@ -1,6 +1,6 @@ import os import sys -from collections import Sequence +from collections.abc import Sequence from glob import glob from os.path import dirname, realpath diff --git a/lib/threads/color.py b/lib/threads/color.py index e24856a9f..320161128 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -2,8 +2,7 @@ import colorsys import re import tinycss2.color3 - -import simplestyle +from inkex import Color from pyembroidery.EmbThread import EmbThread @@ -19,14 +18,14 @@ class ThreadColor(object): self.manufacturer = color.brand self.rgb = (color.get_red(), color.get_green(), color.get_blue()) return - elif isinstance(color, unicode): + elif isinstance(color, str): self.rgb = tinycss2.color3.parse_color(color) # remove alpha channel and multiply with 255 self.rgb = tuple(channel * 255.0 for channel in list(self.rgb)[:-1]) elif isinstance(color, (list, tuple)): self.rgb = tuple(color) elif self.hex_str_re.match(color): - self.rgb = simplestyle.parseColor(color) + self.rgb = Color.parse_str(color)[1] else: raise ValueError("Invalid color: " + repr(color)) @@ -77,7 +76,7 @@ class ThreadColor(object): @property def hex_digits(self): - return "%02X%02X%02X" % self.rgb + return "%02X%02X%02X" % tuple([int(x) for x in self.rgb]) @property def rgb_normalized(self): diff --git a/lib/threads/palette.py b/lib/threads/palette.py index d685e5bbf..c5e3002c1 100644 --- a/lib/threads/palette.py +++ b/lib/threads/palette.py @@ -1,7 +1,8 @@ -from collections import Set -from colormath.color_objects import sRGBColor, LabColor +from collections.abc import Set + from colormath.color_conversions import convert_color from colormath.color_diff import delta_e_cie1994 +from colormath.color_objects import LabColor, sRGBColor from .color import ThreadColor diff --git a/lib/utils/dotdict.py b/lib/utils/dotdict.py index 76f23697a..e946ecd44 100644 --- a/lib/utils/dotdict.py +++ b/lib/utils/dotdict.py @@ -13,7 +13,7 @@ class DotDict(dict): self.dotdictify() def _dotdictify(self): - for k, v in self.iteritems(): + for k, v in self.items(): if isinstance(v, dict): self[k] = DotDict(v) diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index bae32c323..f7b49407e 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -1,6 +1,7 @@ import math -from shapely.geometry import LineString, Point as ShapelyPoint +from shapely.geometry import LineString +from shapely.geometry import Point as ShapelyPoint def cut(line, distance, normalized=False): @@ -123,9 +124,6 @@ class Point: def as_tuple(self): return (self.x, self.y) - def __cmp__(self, other): - return cmp(self.as_tuple(), other.as_tuple()) - def __getitem__(self, item): return self.as_tuple()[item] diff --git a/lib/utils/inkscape.py b/lib/utils/inkscape.py index a650da69c..f89ea447f 100644 --- a/lib/utils/inkscape.py +++ b/lib/utils/inkscape.py @@ -1,10 +1,10 @@ -from os.path import realpath, expanduser, join as path_join import sys +from os.path import expanduser, realpath def guess_inkscape_config_path(): if getattr(sys, 'frozen', None): - path = realpath(path_join(sys._MEIPASS, "..", "..", "..")) + path = realpath(sys._MEIPASS.split('extensions', 1)[0]) if sys.platform == "win32": import win32api diff --git a/lib/utils/io.py b/lib/utils/io.py index f51f629c0..239585f43 100644 --- a/lib/utils/io.py +++ b/lib/utils/io.py @@ -1,15 +1,15 @@ import os import sys -from cStringIO import StringIO +from io import StringIO def save_stderr(): # GTK likes to spam stderr, which inkscape will show in a dialog. - null = open(os.devnull, 'w') - sys.stderr_dup = os.dup(sys.stderr.fileno()) - sys.real_stderr = os.fdopen(sys.stderr_dup, 'w') - os.dup2(null.fileno(), 2) - sys.stderr = StringIO() + with open(os.devnull, 'w') as null: + sys.stderr_dup = os.dup(sys.stderr.fileno()) + sys.real_stderr = os.fdopen(sys.stderr_dup, 'w', encoding='utf-8') + os.dup2(null.fileno(), 2) + sys.stderr = StringIO() def restore_stderr(): @@ -22,11 +22,11 @@ def restore_stderr(): def save_stdout(): - null = open(os.devnull, 'w') - sys.stdout_dup = os.dup(sys.stdout.fileno()) - sys.real_stdout = os.fdopen(sys.stdout_dup, 'w') - os.dup2(null.fileno(), 1) - sys.stdout = StringIO() + with open(os.devnull, 'w') as null: + sys.stdout_dup = os.dup(sys.stdout.fileno()) + sys.real_stdout = os.fdopen(sys.stdout_dup, 'w') + os.dup2(null.fileno(), 1) + sys.stdout = StringIO() def restore_stdout(): diff --git a/lib/utils/version.py b/lib/utils/version.py new file mode 100644 index 000000000..02eb388bc --- /dev/null +++ b/lib/utils/version.py @@ -0,0 +1,17 @@ +import sys +from os.path import isfile, join, realpath + +from ..i18n import _ + + +def get_inkstitch_version(): + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + version = realpath(join(sys._MEIPASS, "..", "VERSION")) + else: + version = realpath(join(realpath(__file__), "..", "..", "..", 'VERSION')) + if isfile(version): + with open(version, 'r') as v: + inkstitch_version = _("Ink/Stitch Version: %s") % v.readline() + else: + inkstitch_version = _("Ink/Stitch Version: unkown") + return inkstitch_version diff --git a/print/resources/inkstitch.js b/print/resources/inkstitch.js index a5d57e265..8bdc4ec04 100644 --- a/print/resources/inkstitch.js +++ b/print/resources/inkstitch.js @@ -150,7 +150,7 @@ function writeEstimatedTime( selector, estimatedTime ) { function scaleSVG(element, scale = 'fit') { // always center svg - transform = "translate(-50%, -50%)"; + var transform = "translate(-50%, -50%)"; if(scale == 'fit') { var scale = Math.min( @@ -278,7 +278,7 @@ $(function() { }) }); - /* Contendeditable Fields */ + /* Contenteditable Fields */ $('body').on('focusout', '[contenteditable="true"]:not(.info-text)', function() { /* change svg scale */ diff --git a/print/resources/style.css b/print/resources/style.css index e1bb857a9..79e34db84 100644 --- a/print/resources/style.css +++ b/print/resources/style.css @@ -384,7 +384,7 @@ body { z-index: 4; position: fixed; width: 50%; - height: 25%%; + height: 25%; top: 200px; left: 25%; background: rgb(255, 255, 255); @@ -906,7 +906,6 @@ body { background: #fff; margin-left: auto; margin-right: auto; - white-space: wrap; text-align: center; padding-top: 2mm; position: relative; diff --git a/print/templates/operator_detailedview.html b/print/templates/operator_detailedview.html index c74a7a728..b16691a0b 100644 --- a/print/templates/operator_detailedview.html +++ b/print/templates/operator_detailedview.html @@ -28,7 +28,7 @@ {{ _('Design box size') }}: {{ "%0.1fmm X %0.1fmm" | format(*job.dimensions) }} {{ _('Total stitch count') }}: {{job.num_stitches }} - {{ ('Estimated time') }}: + {{ _('Estimated time') }}:

{{ _('Total stops') }}: {{ job.num_stops }} diff --git a/pyembroidery b/pyembroidery index c0b817d06..d23ead189 160000 --- a/pyembroidery +++ b/pyembroidery @@ -1 +1 @@ -Subproject commit c0b817d0626140221e707e88be571728c393a3ca +Subproject commit d23ead18908706a5db2365441d4bef6884e99ba9 diff --git a/requirements.txt b/requirements.txt index 3bf490a68..84ba47d0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,19 @@ ./pyembroidery +inkex backports.functools_lru_cache -wxPython==4.0.6 -networkx==2.2 -shapely==1.6.3 +wxPython +networkx +shapely lxml appdirs -numpy<1.16.0 +numpy<=1.17.4 jinja2>2.9 requests colormath stringcase tinycss2 - -# We're not ready for flask 1.0 yet. Logging changed, among othe things. -flask==0.* +flask +fonttools pywinutils; sys.platform == 'win32' pywin32; sys.platform == 'win32' diff --git a/templates/about.xml b/templates/about.xml new file mode 100644 index 000000000..68609d95e --- /dev/null +++ b/templates/about.xml @@ -0,0 +1,28 @@ + + + {% trans %}About{% endtrans %} + org.inkstitch.about.{{ locale }} + + + {{ image_path }}inkstitch_colour_logo.svg + + + + + + + + + +{{ inkstitch_license }} + + + + + all + + + + + + diff --git a/templates/auto_satin.inx b/templates/auto_satin.xml similarity index 100% rename from templates/auto_satin.inx rename to templates/auto_satin.xml diff --git a/templates/break_apart.inx b/templates/break_apart.xml similarity index 100% rename from templates/break_apart.inx rename to templates/break_apart.xml diff --git a/templates/cleanup.inx b/templates/cleanup.xml similarity index 100% rename from templates/cleanup.inx rename to templates/cleanup.xml diff --git a/templates/convert_to_satin.inx b/templates/convert_to_satin.xml similarity index 100% rename from templates/convert_to_satin.inx rename to templates/convert_to_satin.xml diff --git a/templates/cut_satin.inx b/templates/cut_satin.xml similarity index 100% rename from templates/cut_satin.inx rename to templates/cut_satin.xml diff --git a/templates/embroider.inx b/templates/embroider.inx deleted file mode 100644 index b79c2f5fb..000000000 --- a/templates/embroider.inx +++ /dev/null @@ -1,28 +0,0 @@ - - - {% trans %}Embroider{% endtrans %} - org.inkstitch.embroider.{{ locale }} - 3.0 - true - - {% for format, description in formats %} - <_option value="{{ format }}">{{ _(description) }} ({{ format | upper }}) - {% endfor %} - {% for format, description in debug_formats %} - <_option value="{{ format }}">{{ _(description) }} ({{ format | upper }}) [{{ _("DEBUG") }}] - {% endfor %} - - - embroider - - all - - - - - - - - diff --git a/templates/embroider.xml b/templates/embroider.xml new file mode 100644 index 000000000..ff56ac7d6 --- /dev/null +++ b/templates/embroider.xml @@ -0,0 +1,18 @@ + + + {% trans %}Embroider{% endtrans %} + org.inkstitch.embroider.{{ locale }} + {% trans %}Create a stitch file{% endtrans %} + {% trans %}Save your embroidery file through | File > Save a Copy ... |{% endtrans %} + {% trans %}Choose from listed embroidery file formats and save.{% endtrans %} + + {% trans %}Multiple file formats can be saved by choosing the zip file format.{% endtrans %} + + all + + + + + + + diff --git a/templates/embroider_settings.xml b/templates/embroider_settings.xml new file mode 100644 index 000000000..51ce9d16e --- /dev/null +++ b/templates/embroider_settings.xml @@ -0,0 +1,21 @@ + + + {% trans %}Settings{% endtrans %} + org.inkstitch.embroider_settings.{{ locale }} + embroider_settings + + all + + + + + + {% trans %}Output Settings{% endtrans %} + + 3 + + diff --git a/templates/flip.inx b/templates/flip.xml similarity index 100% rename from templates/flip.inx rename to templates/flip.xml diff --git a/templates/global_commands.inx b/templates/global_commands.xml similarity index 100% rename from templates/global_commands.inx rename to templates/global_commands.xml diff --git a/templates/import_threadlist.inx b/templates/import_threadlist.xml similarity index 75% rename from templates/import_threadlist.inx rename to templates/import_threadlist.xml index 081a881a6..5db963b6b 100644 --- a/templates/import_threadlist.inx +++ b/templates/import_threadlist.xml @@ -3,9 +3,7 @@ {% trans %}Import Threadlist{% endtrans %} org.inkstitch.import_threadlist.{{ locale }} import_threadlist - - {{ _("Enter path to file") }} + diff --git a/templates/input.inx b/templates/input.xml similarity index 100% rename from templates/input.inx rename to templates/input.xml diff --git a/templates/install.inx b/templates/install.xml similarity index 92% rename from templates/install.inx rename to templates/install.xml index ffcb9b7b3..764027b83 100644 --- a/templates/install.inx +++ b/templates/install.xml @@ -3,7 +3,7 @@ {% trans %}Install thread color palettes for Inkscape{% endtrans %} org.inkstitch.install.{{ locale }} install - + all diff --git a/templates/layer_commands.inx b/templates/layer_commands.xml similarity index 100% rename from templates/layer_commands.inx rename to templates/layer_commands.xml diff --git a/templates/lettering.inx b/templates/lettering.xml similarity index 92% rename from templates/lettering.inx rename to templates/lettering.xml index 65eee8a11..6dea9e93e 100644 --- a/templates/lettering.inx +++ b/templates/lettering.xml @@ -3,7 +3,7 @@ {% trans %}Lettering{% endtrans %} org.inkstitch.lettering.{{ locale }} lettering - + all diff --git a/templates/lettering_custom_font_dir.xml b/templates/lettering_custom_font_dir.xml new file mode 100644 index 000000000..b08c37d6a --- /dev/null +++ b/templates/lettering_custom_font_dir.xml @@ -0,0 +1,26 @@ + + + {% trans %}Custom font directory{% endtrans %} + org.inkstitch.lettering_custom_font_dir.{{ locale }} + lettering_custom_font_dir + + all + + + + + + + + {% trans %}Set a custom directory for additional fonts to be used with the lettering tool.{% endtrans %} + + + + + + {% trans %}Usage: Create a subdirectory for each font you add.{% endtrans %} + + + diff --git a/templates/lettering_generate_json.xml b/templates/lettering_generate_json.xml new file mode 100644 index 000000000..5f4907f78 --- /dev/null +++ b/templates/lettering_generate_json.xml @@ -0,0 +1,46 @@ + + + {% trans %}Generate JSON{% endtrans %} + org.inkstitch.lettering_generate_json.{{ locale }} + lettering_generate_json + + all + + + + + + + + {% trans %}Generates font.json which can be used by the lettering tool.{% endtrans %} + + + + + + + + + {% trans %}Insert a font SVG file with kerning information.{% endtrans %} + + + + + true + true + + � + + + 1 + 1 + + + + + + diff --git a/templates/lettering_remove_kerning.xml b/templates/lettering_remove_kerning.xml new file mode 100644 index 000000000..5657e003b --- /dev/null +++ b/templates/lettering_remove_kerning.xml @@ -0,0 +1,28 @@ + + + {% trans %}Remove Kerning{% endtrans %} + org.inkstitch.lettering_remove_kerning.{{ locale }} + lettering_remove_kerning + + all + + + + + + + + {% trans %}Removes Kerning information from given SVG files{% endtrans %} + + + + ⚠ {% trans %}Make sure you keep a copy of the original file. After running this extension kerning information will be lost unrevertably from these files.{% endtrans %} + + + + + + + diff --git a/templates/object_commands.inx b/templates/object_commands.xml similarity index 100% rename from templates/object_commands.inx rename to templates/object_commands.xml diff --git a/templates/output.inx b/templates/output.xml similarity index 93% rename from templates/output.inx rename to templates/output.xml index ffff4be97..545c3d286 100644 --- a/templates/output.inx +++ b/templates/output.xml @@ -4,7 +4,7 @@ org.inkstitch.output.{{ format }}.{{ locale }} .{{ format }} - application/x-embroidery-{{ format }} + {{ mimetype }} <_filetypename>Ink/Stitch: {{ _(description) }} (.{{ format }}) <_filetypetooltip>{{ _("Save design in %(file_extension)s format using Ink/Stitch") % dict(file_extension=format.upper()) }} true diff --git a/templates/output_params_txt.xml b/templates/output_params_txt.xml index c8ca35ee6..8b7f66f5a 100644 --- a/templates/output_params_txt.xml +++ b/templates/output_params_txt.xml @@ -36,6 +36,6 @@ gui-description="{{ _("minimum spindle speed value (grbl $31 setting)") }}" min="-1" max="1000000000">-1 -1 - -1 + -1 diff --git a/templates/params.inx b/templates/params.xml similarity index 92% rename from templates/params.inx rename to templates/params.xml index a2dc89a32..4ceb90204 100644 --- a/templates/params.inx +++ b/templates/params.xml @@ -3,7 +3,7 @@ {% trans %}Params{% endtrans %} org.inkstitch.params.{{ locale }} params - + all diff --git a/templates/print.inx b/templates/print.xml similarity index 93% rename from templates/print.inx rename to templates/print.xml index 33d8b25c5..410f9b54c 100644 --- a/templates/print.inx +++ b/templates/print.xml @@ -3,7 +3,7 @@ {% trans %}PDF Export{% endtrans %} org.inkstitch.print.{{ locale }} print - + all diff --git a/templates/remove_embroidery_settings.inx b/templates/remove_embroidery_settings.xml similarity index 100% rename from templates/remove_embroidery_settings.inx rename to templates/remove_embroidery_settings.xml diff --git a/templates/reorder.xml b/templates/reorder.xml new file mode 100644 index 000000000..7e0026163 --- /dev/null +++ b/templates/reorder.xml @@ -0,0 +1,15 @@ + + + {% trans %}Reorder{% endtrans %} + org.inkstitch.reorder.{{ locale }} + reorder + + all + + + + + + diff --git a/templates/simulator.inx b/templates/simulator.xml similarity index 93% rename from templates/simulator.inx rename to templates/simulator.xml index dfa6b34ad..029d8b37b 100644 --- a/templates/simulator.inx +++ b/templates/simulator.xml @@ -3,7 +3,7 @@ {% trans %}Simulator / Realistic Preview{% endtrans %} org.inkstitch.simulator.{{ locale }} simulator - + all diff --git a/templates/stitch_plan_preview.inx b/templates/stitch_plan_preview.xml similarity index 100% rename from templates/stitch_plan_preview.inx rename to templates/stitch_plan_preview.xml diff --git a/templates/troubleshoot.inx b/templates/troubleshoot.xml similarity index 100% rename from templates/troubleshoot.inx rename to templates/troubleshoot.xml diff --git a/templates/zip.inx b/templates/zip.xml similarity index 89% rename from templates/zip.inx rename to templates/zip.xml index 426230190..4d6204244 100644 --- a/templates/zip.inx +++ b/templates/zip.xml @@ -9,8 +9,10 @@ <_filetypetooltip>{{ _("Create a ZIP with multiple embroidery file formats using Ink/Stitch") }} true - {%- for format, description in formats %} + {%- for format, description, mimetype, category in formats %} + {%- if category == "embroidery" %} false + {%- endif %} {%- endfor %} false false