diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..cd0b774
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [Sodium-Hydrogen]
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..d763079
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,61 @@
+name: Report an issue with svg2mod
+description: Report an issue with svg2mod.
+labels: [bug, unconfirmed]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ This issue form is for reporting bugs only!
+
+ If you have a feature or enhancement request, please use the [ideas category][fr] in the github discussions.
+
+ [fr]: https://github.com/svg2mod/svg2mod/discussions/categories/ideas
+
+ - type: textarea
+ validations:
+ required: true
+ attributes:
+ label: The problem
+ description: |
+ Describe the issue you are experiencing here, to communicate to the
+ maintainers. Tell us what you were trying to do and what happened.
+
+ Provide a clear and concise description of what the problem is.
+
+ - type: input
+ validations:
+ required: true
+ attributes:
+ label: Version
+ description: |
+ The version of svg2mod you are using.
+
+ If installed via pip you can use `pip show svg2mod` to get the version.
+
+ Otherwise `git describe --tag` if you installed locally.
+
+ - type: input
+ validations:
+ required: true
+ id: command_line
+ attributes:
+ label: Command line
+ description: >
+ The command that when run will reproduce the error.
+
+ - type: textarea
+ validations:
+ required: true
+ attributes:
+ label: Debug output
+ description: The output of the command. Please use the `--debug` flag.
+ render: txt
+
+ - type: textarea
+ validations:
+ required: true
+ attributes:
+ label: Sample file and additional information
+ description: |
+ __Please attach a sample file__ that will trigger the issue and
+ if you have any additional information for us, use the field below.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..127775b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Feature Request
+ url: https://github.com/svg2mod/svg2mod/discussions/categories/ideas
+ about: Please use our GitHub Discussions board for making feature requests.
+ - name: Questions
+ url: https://github.com/svg2mod/svg2mod/discussions/categories/q-a
+ about: The issue tracker is only for issues. Please keep all other questions and discussions to GitHub Disscussions.
\ No newline at end of file
diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml
new file mode 100644
index 0000000..1fb3c61
--- /dev/null
+++ b/.github/workflows/markdown-lint.yml
@@ -0,0 +1,27 @@
+name: Documentation Linting
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 16.x
+ - name: Installing dependencies
+ run: |
+ npm install -g markdownlint-cli2
+ npm install -g cspell
+ - name: Lint
+ run: markdownlint-cli2-config ".linter/.markdownlint.yaml" "*.md"
+ - name: Spellcheck
+ run: cspell -c .linter/cspell.json "*.md" --show-suggestions
+
\ No newline at end of file
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 0000000..73ffe7b
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,46 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python lint and test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.8", "3.9", "3.10"]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install wheel pytest pylint pyenchant ./
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with pylint
+ run: |
+ pylint --rcfile .linter/cleanup.rc svg2mod setup.py
+ - name: Test with pytest
+ run: |
+ python setup.py test
+ - name: Run svg tests
+ run: |
+ svg2mod -i examples/svg2mod.svg -o output.mod -x -c -P --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug
+ svg2mod -i examples/svg2mod.svg --format legacy --force F.Cu
+ svg2mod -i examples/svg2mod.svg --format legacy -o output.mod
+ svg2mod -i examples/svg2mod.svg --format pretty --debug --force F.Cu -x
+ svg2mod -i examples/svg2mod.svg --debug
+ svg2mod -i examples/svg2mod.svg
+ svg2mod -l
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 0000000..9cb7e23
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,37 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload Python Package
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ deploy:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install wheel setuptools
+ - name: Build package
+ run: python setup.py bdist_wheel sdist
+ - name: Publish package
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
+
diff --git a/.gitignore b/.gitignore
index 24bc31d..d8113fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,18 @@
*~
.*.sw?
*.pyc
+build
+dist
+svg2mod.egg-info
+.eggs
+*.kicad_mod
+*.mod
+*.svg
+__pycache__
+.pytest_cache
+Pipfile
+Pipfile.lock
+.vscode
+*.zip
+*.cmd
+*.sh
\ No newline at end of file
diff --git a/.linter/.markdownlint.yaml b/.linter/.markdownlint.yaml
new file mode 100644
index 0000000..0d39f14
--- /dev/null
+++ b/.linter/.markdownlint.yaml
@@ -0,0 +1,4 @@
+default: true
+
+MD013:
+ line_length: 100
\ No newline at end of file
diff --git a/.linter/cleanup.rc b/.linter/cleanup.rc
new file mode 100644
index 0000000..e74034d
--- /dev/null
+++ b/.linter/cleanup.rc
@@ -0,0 +1,543 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=0.0
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=0
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=all
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member,
+ anomalous-backslash-in-string,
+ fixme,
+ line-too-long,
+ no-else-return,
+ no-else-break,
+ no-else-continue,
+ no-else-raise,
+ useless-object-inheritance,
+ too-many-arguments,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-lines,
+ too-many-locals,
+ too-many-nested-blocks,
+ too-many-statements,
+ too-many-function-args,
+ pointless-string-statement,
+ superfluous-parens,
+ no-self-use,
+ signature-differs,
+ too-many-lines,
+ wrong-import-order,
+ useless-import-alias,
+ continue-in-finally,
+ duplicate-code,
+ pointless-statement,
+ pointless-string-statement,
+ unused-import,
+ unused-variable,
+ unused-argument,
+ unused-wildcard-import,
+ attribute-defined-outside-init,
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=no
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA
+# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA
+# (hunspell).
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=new
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=150
+
+# Maximum number of lines in a module.
+max-module-lines=1500
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/.linter/cspell.json b/.linter/cspell.json
new file mode 100644
index 0000000..9158496
--- /dev/null
+++ b/.linter/cspell.json
@@ -0,0 +1,9 @@
+{
+ "dictionaries": ["custom_dict"],
+ "dictionaryDefinitions": [
+ {
+ "name": "custom_dict",
+ "path": "./custom_dict"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.linter/custom_dict b/.linter/custom_dict
new file mode 100644
index 0000000..662c54d
--- /dev/null
+++ b/.linter/custom_dict
@@ -0,0 +1,68 @@
+adhes
+API
+attrib
+bezier
+cjlano
+Cmap
+cmts
+collinear
+coloredlogger
+coord
+copperpour
+decompiles
+Demibold
+descr
+Douglas
+DPI
+dwgs
+elt
+evenodd
+Faux
+fonttools
+formatter
+getLogger
+init
+inkscape
+inlinable
+inlined
+iterable
+json
+keepout
+kicad
+kipart
+levelno
+otf
+pc
+PCBNEW
+pdistance
+Peucker
+poly
+pre
+precompute
+px
+py
+pyenchant
+Ramer
+rect
+Regex
+rx
+ry
+stderr
+stdout
+streamlitapp
+svg
+sys
+tedit
+thru
+Traceback
+ttf
+txt
+un
+vect
+viewport
+Wikipedia
+xlength
+xml
+xscale
+ylength
+yscale
diff --git a/.linter/required-linting.rc b/.linter/required-linting.rc
new file mode 100644
index 0000000..4ae5779
--- /dev/null
+++ b/.linter/required-linting.rc
@@ -0,0 +1,559 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10.0
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=all
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member,
+ missing-function-docstring,
+ missing-class-docstring,
+ missing-module-docstring,
+ trailing-whitespace,
+ redefined-builtin,
+ bad-indentation,
+ unidiomatic-typecheck,
+ undefined-variable,
+ no-member,
+ dangerous-default-value,
+ expression-not-assigned,
+ invalid-length-returned,
+ invalid-bool-returned,
+ invalid-index-returned,
+ invalid-repr-returned,
+ invalid-str-returned,
+ invalid-bytes-returned,
+ invalid-hash-returned,
+ invalid-length-hint-returned,
+ invalid-format-returned,
+ invalid-getnewargs-returned,
+ invalid-getnewargs-ex-returned,
+ used-before-assignment,
+ raising-bad-type,
+ bad-exception-context,
+ bad-super-call,
+ not-an-iterable,
+ no-method-argument,
+ no-self-argument,
+ invalid-slots-object,
+ assigning-non-slot,
+ invalid-slots,
+ inherit-non-class,
+ empty-docstring,
+ non-ascii-name,
+ unexpected-line-ending-format,
+ syntax-error,
+ unrecognized-inline-option,
+ return-in-init,
+ duplicate-argument-name,
+ abstract-class-instantiated,
+ not-callable,
+ assignment-from-no-return,
+ fatal,
+ parse-error,
+ wrong-spelling-in-comment,
+ wrong-spelling-in-docstring,
+ invalid-characters-in-docstring
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=no
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA
+# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA
+# (hunspell).
+spelling-dict=en_US
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=.linter/custom_dict
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/LICENSE b/LICENSE
index 2c4afab..ecbc059 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,117 +1,339 @@
-CC0 1.0 Universal
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator and
-subsequent owner(s) (each and all, an "owner") of an original work of
-authorship and/or a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for the
-purpose of contributing to a commons of creative, cultural and scientific
-works ("Commons") that the public can reliably and without fear of later
-claims of infringement build upon, modify, incorporate in other works, reuse
-and redistribute as freely as possible in any form whatsoever and for any
-purposes, including without limitation commercial purposes. These owners may
-contribute to the Commons to promote the ideal of a free culture and the
-further production of creative, cultural and scientific works, or to gain
-reputation or greater distribution for their Work in part through the use and
-efforts of others.
-
-For these and/or other purposes and motivations, and without any expectation
-of additional consideration or compensation, the person associating CC0 with a
-Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
-and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
-and publicly distribute the Work under its terms, with knowledge of his or her
-Copyright and Related Rights in the Work and the meaning and intended legal
-effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be
-protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not limited
-to, the following:
-
- i. the right to reproduce, adapt, distribute, perform, display, communicate,
- and translate a Work;
-
- ii. moral rights retained by the original author(s) and/or performer(s);
-
- iii. publicity and privacy rights pertaining to a person's image or likeness
- depicted in a Work;
-
- iv. rights protecting against unfair competition in regards to a Work,
- subject to the limitations in paragraph 4(a), below;
-
- v. rights protecting the extraction, dissemination, use and reuse of data in
- a Work;
-
- vi. database rights (such as those arising under Directive 96/9/EC of the
- European Parliament and of the Council of 11 March 1996 on the legal
- protection of databases, and under any national implementation thereof,
- including any amended or successor version of such directive); and
-
- vii. other similar, equivalent or corresponding rights throughout the world
- based on applicable law or treaty, and any national implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention of,
-applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
-unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
-and Related Rights and associated claims and causes of action, whether now
-known or unknown (including existing as well as future claims and causes of
-action), in the Work (i) in all territories worldwide, (ii) for the maximum
-duration provided by applicable law or treaty (including future time
-extensions), (iii) in any current or future medium and for any number of
-copies, and (iv) for any purpose whatsoever, including without limitation
-commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
-the Waiver for the benefit of each member of the public at large and to the
-detriment of Affirmer's heirs and successors, fully intending that such Waiver
-shall not be subject to revocation, rescission, cancellation, termination, or
-any other legal or equitable action to disrupt the quiet enjoyment of the Work
-by the public as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason be
-judged legally invalid or ineffective under applicable law, then the Waiver
-shall be preserved to the maximum extent permitted taking into account
-Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
-is so judged Affirmer hereby grants to each affected person a royalty-free,
-non transferable, non sublicensable, non exclusive, irrevocable and
-unconditional license to exercise Affirmer's Copyright and Related Rights in
-the Work (i) in all territories worldwide, (ii) for the maximum duration
-provided by applicable law or treaty (including future time extensions), (iii)
-in any current or future medium and for any number of copies, and (iv) for any
-purpose whatsoever, including without limitation commercial, advertising or
-promotional purposes (the "License"). The License shall be deemed effective as
-of the date CC0 was applied by Affirmer to the Work. Should any part of the
-License for any reason be judged legally invalid or ineffective under
-applicable law, such partial invalidity or ineffectiveness shall not
-invalidate the remainder of the License, and in such case Affirmer hereby
-affirms that he or she will not (i) exercise any of his or her remaining
-Copyright and Related Rights in the Work or (ii) assert any associated claims
-and causes of action with respect to the Work, in either case contrary to
-Affirmer's express Statement of Purpose.
-
-4. Limitations and Disclaimers.
-
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
- surrendered, licensed or otherwise affected by this document.
-
- b. Affirmer offers the Work as-is and makes no representations or warranties
- of any kind concerning the Work, express, implied, statutory or otherwise,
- including without limitation warranties of title, merchantability, fitness
- for a particular purpose, non infringement, or the absence of latent or
- other defects, accuracy, or the present or absence of errors, whether or not
- discoverable, all to the greatest extent permissible under applicable law.
-
- c. Affirmer disclaims responsibility for clearing rights of other persons
- that may apply to the Work or any use thereof, including without limitation
- any person's Copyright and Related Rights in the Work. Further, Affirmer
- disclaims responsibility for obtaining any necessary consents, permissions
- or other rights required for any use of the Work.
-
- d. Affirmer understands and acknowledges that Creative Commons is not a
- party to this document and has no duty or obligation with respect to this
- CC0 or use of the Work.
-
-For more information, please see
-
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
\ No newline at end of file
diff --git a/README.md b/README.md
index d048ea5..ac05603 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,198 @@
# svg2mod
-This is a small program to convert Inkscape SVG drawings to KiCad footprint module files. It uses [cjlano's python SVG parser and drawing module](https://github.com/cjlano/svg) to interpret drawings and approximate curves using straight line segments. Module files can be output in KiCad's legacy or s-expression (i.e., pretty) formats. Horizontally mirrored modules are automatically generated for use on the back of a 2-layer PCB.
+
+[](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml)
+[](https://github.com/svg2mod/svg2mod/commits/main)
+
+[](https://pypi.org/project/svg2mod/)
+[](https://pypi.org/project/svg2mod/)
+[](https://pypi.org/project/svg2mod/)
+
+[](https://pypi.org/project/svg2mod/)
+
+[](https://github.com/sponsors/Sodium-Hydrogen)
+
+This is a program / library to convert SVG drawings to KiCad footprint module files.
+
+It includes a modified version of [cjlano's python SVG parser and drawing module](https://github.com/cjlano/svg)
+to interpret drawings and approximate curves using straight line segments. Module files can be
+output in KiCad's legacy or s-expression (i.e., pretty) formats.
+
+## Requirements
+
+* Python 3
+* [fonttools](https://pypi.org/project/fonttools/)
+
+## Installation
+
+```pip install svg2mod```
+
+### Don't want to install the application?
+
+You can use a 3rd party web application instead:
+
+* [svg2mod.streamlitapp.com](https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/)
+* [svg2mod.com](https://svg2mod.com/)
+
+## Showcase
+
+We'd love to see the amazing projects that use svg2mod.
+
+If you have a project you are proud of please post about it on our
+[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell)
+
+[](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell)
+
+## Example
+
+```svg2mod input.svg```
## Usage
-```
-usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE]
- [-f FACTOR] [-p PRECISION] [-d DPI] [--front-only] [--format FORMAT]
- [--units UNITS]
+
+```text
+usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x]
+ [--force LAYER] [-d DPI] [-f FACTOR] [-p PRECISION]
+ [--format FORMAT] [--name NAME] [--units UNITS] [--value VALUE]
+ [-F DEFAULT_FONT] [-l]
+ [IN_FILENAME]
Convert Inkscape SVG drawings to KiCad footprint modules.
+positional arguments:
+ IN_FILENAME Name of the SVG file
+
optional arguments:
-h, --help show this help message and exit
-i FILENAME, --input-file FILENAME
- name of the SVG file
+ Name of the SVG file, but specified with a flag.
-o FILENAME, --output-file FILENAME
- name of the module file
- --name NAME, --module-name NAME
- base name of the module
- --value VALUE, --module-value VALUE
- value of the module
+ Name of the module file
+ -c, --center Center the module to the center of the bounding box
+ -P, --convert-pads Convert any artwork on Cu layers to pads
+ -v, --verbose Print more verbose messages
+ --debug Print debug level messages
+ -x, --exclude-hidden Do not export hidden objects
+ --force LAYER, --force-layer LAYER
+ Force everything into the single provided layer
+ -d DPI, --dpi DPI DPI of the SVG file (int)
-f FACTOR, --factor FACTOR
- scale paths by this factor
+ Scale paths by this factor
-p PRECISION, --precision PRECISION
- smoothness for approximating curves with line segments
- (float)
- -d DPI, --dpi DPI DPI of the SVG file (int)
- --front-only omit output of back module (legacy output format)
- --format FORMAT output module file format (legacy|pretty)
- --units UNITS output units, if output format is legacy (decimil|mm)
+ Smoothness for approximating curves with line
+ segments. Input is the approximate length for each
+ line segment in SVG pixels (float)
+ --format FORMAT Output module file format (legacy|pretty|latest).
+ 'latest' introduces features used in kicad >= 6
+ --name NAME, --module-name NAME
+ Base name of the module
+ --units UNITS Output units, if output format is legacy (decimal|mm)
+ --value VALUE, --module-value VALUE
+ Value of the module
+ -F DEFAULT_FONT, --default-font DEFAULT_FONT
+ Default font to use if the target font in a text
+ element cannot be found
+ -l, --list-fonts List all fonts that can be found in common locations
```
## SVG Files
-svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format.
- * Drawings should be to scale (1 mm in Inscape will be 1 mm in KiCad). Use the --factor option to resize the resulting module(s) up or down from there.
- * Paths are supported.
- * A path may have an outline and a fill. (Colors will be ignored.)
- * A path may have holes, defined by interior segments within the path (see included examples). Sometimes this will render propery in KiCad, but sometimes not.
- * Paths with filled areas within holes may not work at all.
- * Groups may be used. However, styles applied to groups (e.g., stroke-width) are not applied to contained drawing elements. In these cases, it may be necessary to ungroup (and perhaps regroup) the elements.
- * Layers must be used to indicate the mapping of drawing elements to KiCad layers.
- * Layers must be named according to the rules below.
- * Drawing elements will be mapped to front layers by default. Mirrored images of these elements can be automatically generated and mapped to back layers in a separate module (see --front-only option).
- * Other types of elements such as rect, arc, and circle are not supported.
- * Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work.
+svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This
+is so it can associate inkscape layers with kicad layers
+
+* Drawings should be to scale (1 mm in Inkscape will be 1 mm in KiCad). Use the --factor option to
+resize the resulting module(s) up or down from there.
+
+* Most elements are fully supported.
+ * A path may have an outline and a fill. (Colors will be ignored.)
+ * A path may have holes, defined by interior segments within the path (see included examples).
+ * 100% Transparent fills and strokes with be ignored.
+ * Text Elements are partially supported
+* Groups may be used. Styles applied to groups (e.g., stroke-width) are applied to contained drawing
+ elements.
+
+* Layers or items must be named to match the target in kicad. The supported layers are listed below.
+ They will be ignored otherwise.
+ * These are pulled from `inkscape:label` but will pull from `id` if the label isn't set.
+
+* __If there is an issue parsing an inkscape object or stroke convert it to a path.__
+ * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these__
+ __elements into paths that will work.__
### Layers
-Layers must be named (case-insensitive) according to the following rules:
-
-| Inkscape layer name | KiCad layer(s) | KiCad legacy | KiCad pretty |
-|:-------------------:|:----------------:|:------------:|:------------:|
-| Cu | F.Cu, B.Cu | Yes | Yes |
-| Adhes | F.Adhes, B.Adhes | Yes | Yes |
-| Paste | F.Paste, B.Paste | Yes | Yes |
-| SilkS | F.SilkS, B.SilkS | Yes | Yes |
-| Mask | F.Mask, B.Mask | Yes | Yes |
-| Dwgs.User | Dwgs.User | Yes | -- |
-| Cmts.User | Cmts.User | Yes | -- |
-| Eco1.User | Eco1.User | Yes | -- |
-| Eco2.User | Eco2.User | Yes | -- |
-| Edge.Cuts | Edge.Cuts | Yes | Yes |
-| Fab | F.Fab, B.Fab | -- | Yes |
-| CrtYd | F.CrtYd, B.CrtYd | -- | Yes |
-
-Note: If you have a layer "Cu", all of its sub-layers will be treated as "Cu" regardless of their names.
+
+This supports the layers listed below. They are the same in inkscape and kicad:
+
+| KiCad layer(s) | KiCad legacy | KiCad pretty |
+|:--------------------:|:------------:|:------------:|
+| F.Cu [^1] | Yes | Yes |
+| B.Cu [^1] | Yes | Yes |
+| F.Adhes | Yes | Yes |
+| B.Adhes | Yes | Yes |
+| F.Paste | Yes | Yes |
+| B.Paste | Yes | Yes |
+| F.SilkS | Yes | Yes |
+| B.SilkS | Yes | Yes |
+| F.Mask | Yes | Yes |
+| B.Mask | Yes | Yes |
+| Dwgs.User | Yes | Yes |
+| Cmts.User | Yes | Yes |
+| Eco1.User | Yes | Yes |
+| Eco2.User | Yes | Yes |
+| Edge.Cuts | Yes | Yes |
+| F.Fab | -- | Yes |
+| B.Fab | -- | Yes |
+| F.CrtYd | -- | Yes |
+| B.CrtYd | -- | Yes |
+| Drill.Cu [^1] [^2] | -- | Yes |
+| Drill.Mech [^1] [^2] | -- | Yes |
+| *.Keepout [^1] [^4] | -- | Yes [^3] |
+
+Note: If you have a layer `F.Cu`, all of its sub-layers will be treated as `F.Cu` regardless of their
+names.
+
+### Layer Options
+
+Some layers can have options when saving to the newer 'pretty' format.
+
+The options are separated from the layer name by `:`. Ex `F.Cu:...`
+
+Some options can have arguments which are also separated from
+the option key by `:`. If an option has more than one argument they
+are separated by a comma. Ex: `F.Cu:Pad:1,mask`.
+
+If a layer has more than one option they will be separated by `;`
+Ex: `F.Cu:pad;...`
+
+Supported Arguments:
+
+* Pad
+
+ Any copper layer can have the pad specified.
+ The pad option can be used solo (`F.Cu:Pad`) or it can also have it's own arguments.
+ The arguments are:
+
+ * Number
+ If it is set it will specify the number of the pad. Ex: `Pad:1`
+
+ * Paste _(Not available for `Drill.Cu`)_
+ * Mask _(Not available for `Drill.Cu`)_
+
+* Allowed
+
+ Keepout areas will prevent anything from being placed inside them.
+ To allow some things to be placed inside the keepout zone a comma
+ separated list of any of the following options can be used:
+ `tracks`,`vias`,`pads`,`copperpour`,`footprints`
+
+* Hatch
+
+ Keepout areas have different hatching styles. This allows customization
+ of the appearance of hatching when converting from an svg, Ex: `F.Keepout:Hatch:edge`.
+ All available hatch options are `none`, `edge`, `full`.
+
+[^1]: These layers can have arguments when svg2mod is in pretty mode
+
+[^2]: Drills can only be svg circle objects. The stroke width in `Drill.Cu` is the pad size and the fill is the drill size.
+
+[^3]: Only works in Kicad versions >= v6 (`--format latest`).
+
+[^4]: The \* can be { \*, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal.
diff --git a/examples/dt-logo.mod b/examples/dt-logo.mod
deleted file mode 100644
index ca01a35..0000000
--- a/examples/dt-logo.mod
+++ /dev/null
@@ -1,500 +0,0 @@
-PCBNEW-LibModule-V1 Wed 23 Sep 2015 12:56:11 PM
-Units mm
-$INDEX
-DT-Logo-Front
-DT-Logo-Back
-$EndINDEX
-#
-# dt-logo.svg
-#
-$MODULE DT-Logo-Front
-Po 0 0 0 15 00000000 00000000 ~~
-Li DT-Logo-Front
-T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Front"
-T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***"
-DP 0 0 0 0 45 0.00254 23
-Dl -1.11500002778 -1.43850007222
-Dl 1.11500002778 -1.43850007222
-Dl 1.21826499778 -1.43021840222
-Dl 1.31612738778 -1.40623191222
-Dl 1.40729941778 -1.36782838222
-Dl 1.49049330778 -1.31629559222
-Dl 1.56442127778 -1.25292132222
-Dl 1.62779554778 -1.17899335222
-Dl 1.67932833778 -1.09579946222
-Dl 1.71773186778 -1.00462743222
-Dl 1.74171835778 -0.906765042222
-Dl 1.75000002778 -0.803500072222
-Dl 1.75000002778 0.803499987556
-Dl 1.74171835778 0.906764959926
-Dl 1.71773186778 1.00462735636
-Dl 1.67932833778 1.09579939584
-Dl 1.62779554778 1.17899329736
-Dl 1.56442127778 1.25292127989
-Dl 1.49049330778 1.31629556242
-Dl 1.40729941778 1.36782836393
-Dl 1.31612738778 1.40623190342
-Dl 1.21826499778 1.43021839985
-Dl 1.11500002778 1.43850007222
-Dl -1.11500002778 1.43850007222
-Dl -1.21826499778 1.43021839985
-Dl -1.31612738778 1.40623190342
-Dl -1.40729941778 1.36782836393
-Dl -1.49049330778 1.31629556242
-Dl -1.56442127778 1.25292127989
-Dl -1.62779554778 1.17899329736
-Dl -1.67932833778 1.09579939584
-Dl -1.71773186778 1.00462735636
-Dl -1.74171835778 0.906764959926
-Dl -1.75000002778 0.803499987556
-Dl -1.75000002778 -0.803500072222
-Dl -1.74171835778 -0.906765042222
-Dl -1.71773186778 -1.00462743222
-Dl -1.67932833778 -1.09579946222
-Dl -1.62779554778 -1.17899335222
-Dl -1.56442127778 -1.25292132222
-Dl -1.49049330778 -1.31629559222
-Dl -1.40729941778 -1.36782838222
-Dl -1.31612738778 -1.40623191222
-Dl -1.21826499778 -1.43021840222
-Dl -1.11500002778 -1.43850007222
-DP 0 0 0 0 45 0.00254 15
-Dl -0.799826470222 -0.551812572889
-Dl -0.799826470222 0.551656560444
-Dl -0.565762111778 0.551656560444
-Dl -0.561958169057 0.551588736518
-Dl -0.557617048171 0.551401158364
-Dl -0.552923190034 0.551117666424
-Dl -0.548061035561 0.550762101138
-Dl -0.543215025667 0.550358302944
-Dl -0.538569601266 0.549930112284
-Dl -0.534309203273 0.549501369598
-Dl -0.530618272603 0.549095915324
-Dl -0.52768125017 0.548737589904
-Dl -0.525682576889 0.548450233778
-Dl -0.492995048016 0.540568430728
-Dl -0.462283746331 0.52830719253
-Dl -0.433893631435 0.512020431606
-Dl -0.40816966293 0.49206206038
-Dl -0.385456800417 0.468785991278
-Dl -0.366100003497 0.442546136722
-Dl -0.350444231772 0.413696409136
-Dl -0.338834444843 0.382590720946
-Dl -0.331615602311 0.349582984574
-Dl -0.329132663778 0.315027112444
-Dl -0.329132663778 -0.315022850889
-Dl -0.331662301481 -0.349341511769
-Dl -0.339000486336 -0.381745179104
-Dl -0.350771125962 -0.412034607335
-Dl -0.366598127977 -0.440010550903
-Dl -0.3861054 -0.46547376425
-Dl -0.40891684965 -0.488225001817
-Dl -0.434656384545 -0.508065018045
-Dl -0.462947912304 -0.524794567376
-Dl -0.493415340546 -0.538214404251
-Dl -0.525682576889 -0.548125283111
-Dl -0.528861655226 -0.548802540244
-Dl -0.532532627931 -0.549415840178
-Dl -0.536580107545 -0.549963455711
-Dl -0.54088870661 -0.550443659644
-Dl -0.545343037667 -0.550854724778
-Dl -0.549827713257 -0.551194923911
-Dl -0.554227345922 -0.551462529844
-Dl -0.558426548203 -0.551655815378
-Dl -0.562309932641 -0.551773053311
-Dl -0.565762111778 -0.551812516444
-Dl -0.799826470222 -0.551812572889
-DP 0 0 0 0 75 0.00254 15
-Dl -1.11853875578 -1.0274762681
-Dl -1.13990576822 -1.02618187506
-Dl -1.16015910005 -1.02243221638
-Dl -1.17903130383 -1.01642757278
-Dl -1.19625493213 -1.00836822494
-Dl -1.2115625375 -0.998454453586
-Dl -1.22468667251 -0.9868865394
-Dl -1.23535988973 -0.973864763088
-Dl -1.24331474171 -0.959589405351
-Dl -1.24828378102 -0.944260746888
-Dl -1.24999956022 -0.9280790684
-Dl -1.24999956022 0.927762714222
-Dl -1.24828378102 0.943953377266
-Dl -1.24331474171 0.959306407746
-Dl -1.23535988973 0.973617677884
-Dl -1.22468667251 0.986683059904
-Dl -1.2115625375 0.998298426028
-Dl -1.19625493213 1.00825964848
-Dl -1.17903130383 1.01636259948
-Dl -1.16015910005 1.02240315125
-Dl -1.13990576822 1.02617717602
-Dl -1.11853875578 1.027480546
-Dl -0.0133061004444 1.027480546
-Dl 0.008060891422 1.02617717602
-Dl 0.0283142113138 1.02240315125
-Dl 0.0471864102696 1.01636259948
-Dl 0.064410039328 1.00825964848
-Dl 0.0797176495278 0.998298426028
-Dl 0.0928417919076 0.986683059904
-Dl 0.103515017506 0.973617677884
-Dl 0.111469877362 0.959306407746
-Dl 0.116438922514 0.943953377266
-Dl 0.118154704 0.927762714222
-Dl 0.118154704 -0.9280790684
-Dl 0.116438922514 -0.944260746888
-Dl 0.111469877362 -0.959589405351
-Dl 0.103515017506 -0.973864763088
-Dl 0.0928417919076 -0.9868865394
-Dl 0.0797176495278 -0.998454453586
-Dl 0.064410039328 -1.00836822494
-Dl 0.0471864102696 -1.01642757278
-Dl 0.0283142113138 -1.02243221638
-Dl 0.008060891422 -1.02618187506
-Dl -0.0133061004444 -1.0274762681
-Dl -1.11853875578 -1.0274762681
-Dl -0.997979580533 -0.743713298222
-Dl -0.614498785111 -0.743713298222
-Dl -0.517025410222 -0.743713298222
-Dl -0.511574570222 -0.743713298222
-Dl -0.450075184458 -0.738785629162
-Dl -0.391801897737 -0.724512564011
-Dl -0.337519621914 -0.701659015808
-Dl -0.287993268846 -0.670989897596
-Dl -0.243987750389 -0.633270122417
-Dl -0.206267978398 -0.58926460331
-Dl -0.17559886473 -0.539738253318
-Dl -0.152745321241 -0.485455985483
-Dl -0.138472259786 -0.427182712844
-Dl -0.133544592222 -0.365683348444
-Dl -0.133544592222 0.365366977333
-Dl -0.138472259786 0.42687531338
-Dl -0.152745321241 0.485172952267
-Dl -0.17559886473 0.539491132513
-Dl -0.206267978398 0.58906109264
-Dl -0.243987750389 0.633114071167
-Dl -0.287993268846 0.670881306613
-Dl -0.337519621914 0.7015940375
-Dl -0.391801897737 0.724483502347
-Dl -0.450075184458 0.738780939673
-Dl -0.511574570222 0.743717588
-Dl -0.517025410222 0.743717588
-Dl -0.614498785111 0.743717588
-Dl -0.997979580533 0.743717588
-Dl -0.997979580533 -0.743713298222
-Dl -1.11853875578 -1.0274762681
-DP 0 0 0 0 70 0.00254 15
-Dl 0.391977936444 -0.747777337733
-Dl 0.382259953651 -0.746732845899
-Dl 0.373038393029 -0.74370875169
-Dl 0.364437270418 -0.738869127051
-Dl 0.356580601657 -0.732378043925
-Dl 0.349592402583 -0.724399574256
-Dl 0.343596689036 -0.715097789986
-Dl 0.338717476855 -0.70463676306
-Dl 0.335078781877 -0.693180565421
-Dl 0.332804619942 -0.680893269013
-Dl 0.332019006889 -0.667938945778
-Dl 0.332019006889 -0.624653084222
-Dl 0.332804618418 -0.611698760513
-Dl 0.335078776459 -0.599411462818
-Dl 0.338717466187 -0.587955263282
-Dl 0.34359667278 -0.577494234053
-Dl 0.349592381417 -0.568192447278
-Dl 0.356580577273 -0.560213975102
-Dl 0.364437245526 -0.553722889673
-Dl 0.373038371355 -0.548883263138
-Dl 0.382259939935 -0.545859167642
-Dl 0.391977936444 -0.544814675333
-Dl 0.693055256 -0.544814675333
-Dl 0.693055256 0.671999333333
-Dl 0.694042585976 0.684300999329
-Dl 0.69690207426 0.69596052674
-Dl 0.701479968386 0.706824163099
-Dl 0.70762251589 0.716738155943
-Dl 0.715175964306 0.725548752806
-Dl 0.723986561168 0.733102201221
-Dl 0.733900554012 0.739244748725
-Dl 0.744764190372 0.743822642852
-Dl 0.756423717782 0.746682131135
-Dl 0.768725383778 0.747669461111
-Dl 0.822912868889 0.747669461111
-Dl 0.835214534885 0.746682131135
-Dl 0.846874062295 0.743822642852
-Dl 0.857737698655 0.739244748725
-Dl 0.867651691499 0.733102201221
-Dl 0.876462288361 0.725548752806
-Dl 0.884015736777 0.716738155943
-Dl 0.890158284281 0.706824163099
-Dl 0.894736178407 0.69596052674
-Dl 0.897595666691 0.684300999329
-Dl 0.898582996667 0.671999333333
-Dl 0.898582996667 -0.544814675333
-Dl 1.19004122333 -0.544814675333
-Dl 1.19975921378 -0.545859167642
-Dl 1.20898078052 -0.548883263138
-Dl 1.2175819079 -0.553722889673
-Dl 1.22543858025 -0.560213975102
-Dl 1.23242678189 -0.568192447278
-Dl 1.23842249716 -0.577494234053
-Dl 1.24330171038 -0.587955263282
-Dl 1.2469404059 -0.599411462818
-Dl 1.24921456803 -0.611698760513
-Dl 1.25000018111 -0.624653084222
-Dl 1.25000018111 -0.667938945778
-Dl 1.24921456955 -0.680893269013
-Dl 1.24694041132 -0.693180565421
-Dl 1.24330172105 -0.70463676306
-Dl 1.23842251341 -0.715097789986
-Dl 1.23242680306 -0.724399574256
-Dl 1.22543860463 -0.732378043925
-Dl 1.21758193279 -0.738869127051
-Dl 1.2089808022 -0.74370875169
-Dl 1.19975922749 -0.746732845899
-Dl 1.19004122333 -0.747777337733
-Dl 0.391977936444 -0.747777337733
-Dl 0.391977936444 -0.747777337733
-$EndMODULE DT-Logo-Front
-$MODULE DT-Logo-Back
-Po 0 0 0 15 00000000 00000000 ~~
-Li DT-Logo-Back
-T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Back"
-T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***"
-DP 0 0 0 0 45 0.00254 22
-Dl 1.11500002778 -1.43850007222
-Dl -1.11500002778 -1.43850007222
-Dl -1.21826499778 -1.43021840222
-Dl -1.31612738778 -1.40623191222
-Dl -1.40729941778 -1.36782838222
-Dl -1.49049330778 -1.31629559222
-Dl -1.56442127778 -1.25292132222
-Dl -1.62779554778 -1.17899335222
-Dl -1.67932833778 -1.09579946222
-Dl -1.71773186778 -1.00462743222
-Dl -1.74171835778 -0.906765042222
-Dl -1.75000002778 -0.803500072222
-Dl -1.75000002778 0.803499987556
-Dl -1.74171835778 0.906764959926
-Dl -1.71773186778 1.00462735636
-Dl -1.67932833778 1.09579939584
-Dl -1.62779554778 1.17899329736
-Dl -1.56442127778 1.25292127989
-Dl -1.49049330778 1.31629556242
-Dl -1.40729941778 1.36782836393
-Dl -1.31612738778 1.40623190342
-Dl -1.21826499778 1.43021839985
-Dl -1.11500002778 1.43850007222
-Dl 1.11500002778 1.43850007222
-Dl 1.21826499778 1.43021839985
-Dl 1.31612738778 1.40623190342
-Dl 1.40729941778 1.36782836393
-Dl 1.49049330778 1.31629556242
-Dl 1.56442127778 1.25292127989
-Dl 1.62779554778 1.17899329736
-Dl 1.67932833778 1.09579939584
-Dl 1.71773186778 1.00462735636
-Dl 1.74171835778 0.906764959926
-Dl 1.75000002778 0.803499987556
-Dl 1.75000002778 -0.803500072222
-Dl 1.74171835778 -0.906765042222
-Dl 1.71773186778 -1.00462743222
-Dl 1.67932833778 -1.09579946222
-Dl 1.62779554778 -1.17899335222
-Dl 1.56442127778 -1.25292132222
-Dl 1.49049330778 -1.31629559222
-Dl 1.40729941778 -1.36782838222
-Dl 1.31612738778 -1.40623191222
-Dl 1.21826499778 -1.43021840222
-Dl 1.11500002778 -1.43850007222
-DP 0 0 0 0 45 0.00254 0
-Dl 0.799826470222 -0.551812572889
-Dl 0.799826470222 0.551656560444
-Dl 0.565762111778 0.551656560444
-Dl 0.561958169057 0.551588736518
-Dl 0.557617048171 0.551401158364
-Dl 0.552923190034 0.551117666424
-Dl 0.548061035561 0.550762101138
-Dl 0.543215025667 0.550358302944
-Dl 0.538569601266 0.549930112284
-Dl 0.534309203273 0.549501369598
-Dl 0.530618272603 0.549095915324
-Dl 0.52768125017 0.548737589904
-Dl 0.525682576889 0.548450233778
-Dl 0.492995048016 0.540568430728
-Dl 0.462283746331 0.52830719253
-Dl 0.433893631435 0.512020431606
-Dl 0.40816966293 0.49206206038
-Dl 0.385456800417 0.468785991278
-Dl 0.366100003497 0.442546136722
-Dl 0.350444231772 0.413696409136
-Dl 0.338834444843 0.382590720946
-Dl 0.331615602311 0.349582984574
-Dl 0.329132663778 0.315027112444
-Dl 0.329132663778 -0.315022850889
-Dl 0.331662301481 -0.349341511769
-Dl 0.339000486336 -0.381745179104
-Dl 0.350771125962 -0.412034607335
-Dl 0.366598127977 -0.440010550903
-Dl 0.3861054 -0.46547376425
-Dl 0.40891684965 -0.488225001817
-Dl 0.434656384545 -0.508065018045
-Dl 0.462947912304 -0.524794567376
-Dl 0.493415340546 -0.538214404251
-Dl 0.525682576889 -0.548125283111
-Dl 0.528861655226 -0.548802540244
-Dl 0.532532627931 -0.549415840178
-Dl 0.536580107545 -0.549963455711
-Dl 0.54088870661 -0.550443659644
-Dl 0.545343037667 -0.550854724778
-Dl 0.549827713257 -0.551194923911
-Dl 0.554227345922 -0.551462529844
-Dl 0.558426548203 -0.551655815378
-Dl 0.562309932641 -0.551773053311
-Dl 0.565762111778 -0.551812516444
-Dl 0.799826470222 -0.551812572889
-DP 0 0 0 0 75 0.00254 0
-Dl 1.11853875578 -1.0274762681
-Dl 1.13990576822 -1.02618187506
-Dl 1.16015910005 -1.02243221638
-Dl 1.17903130383 -1.01642757278
-Dl 1.19625493213 -1.00836822494
-Dl 1.2115625375 -0.998454453586
-Dl 1.22468667251 -0.9868865394
-Dl 1.23535988973 -0.973864763088
-Dl 1.24331474171 -0.959589405351
-Dl 1.24828378102 -0.944260746888
-Dl 1.24999956022 -0.9280790684
-Dl 1.24999956022 0.927762714222
-Dl 1.24828378102 0.943953377266
-Dl 1.24331474171 0.959306407746
-Dl 1.23535988973 0.973617677884
-Dl 1.22468667251 0.986683059904
-Dl 1.2115625375 0.998298426028
-Dl 1.19625493213 1.00825964848
-Dl 1.17903130383 1.01636259948
-Dl 1.16015910005 1.02240315125
-Dl 1.13990576822 1.02617717602
-Dl 1.11853875578 1.027480546
-Dl 0.0133061004444 1.027480546
-Dl -0.008060891422 1.02617717602
-Dl -0.0283142113138 1.02240315125
-Dl -0.0471864102696 1.01636259948
-Dl -0.064410039328 1.00825964848
-Dl -0.0797176495278 0.998298426028
-Dl -0.0928417919076 0.986683059904
-Dl -0.103515017506 0.973617677884
-Dl -0.111469877362 0.959306407746
-Dl -0.116438922514 0.943953377266
-Dl -0.118154704 0.927762714222
-Dl -0.118154704 -0.9280790684
-Dl -0.116438922514 -0.944260746888
-Dl -0.111469877362 -0.959589405351
-Dl -0.103515017506 -0.973864763088
-Dl -0.0928417919076 -0.9868865394
-Dl -0.0797176495278 -0.998454453586
-Dl -0.064410039328 -1.00836822494
-Dl -0.0471864102696 -1.01642757278
-Dl -0.0283142113138 -1.02243221638
-Dl -0.008060891422 -1.02618187506
-Dl 0.0133061004444 -1.0274762681
-Dl 1.11853875578 -1.0274762681
-Dl 0.997979580533 -0.743713298222
-Dl 0.614498785111 -0.743713298222
-Dl 0.517025410222 -0.743713298222
-Dl 0.511574570222 -0.743713298222
-Dl 0.450075184458 -0.738785629162
-Dl 0.391801897737 -0.724512564011
-Dl 0.337519621914 -0.701659015808
-Dl 0.287993268846 -0.670989897596
-Dl 0.243987750389 -0.633270122417
-Dl 0.206267978398 -0.58926460331
-Dl 0.17559886473 -0.539738253318
-Dl 0.152745321241 -0.485455985483
-Dl 0.138472259786 -0.427182712844
-Dl 0.133544592222 -0.365683348444
-Dl 0.133544592222 0.365366977333
-Dl 0.138472259786 0.42687531338
-Dl 0.152745321241 0.485172952267
-Dl 0.17559886473 0.539491132513
-Dl 0.206267978398 0.58906109264
-Dl 0.243987750389 0.633114071167
-Dl 0.287993268846 0.670881306613
-Dl 0.337519621914 0.7015940375
-Dl 0.391801897737 0.724483502347
-Dl 0.450075184458 0.738780939673
-Dl 0.511574570222 0.743717588
-Dl 0.517025410222 0.743717588
-Dl 0.614498785111 0.743717588
-Dl 0.997979580533 0.743717588
-Dl 0.997979580533 -0.743713298222
-Dl 1.11853875578 -1.0274762681
-DP 0 0 0 0 70 0.00254 0
-Dl -0.391977936444 -0.747777337733
-Dl -0.382259953651 -0.746732845899
-Dl -0.373038393029 -0.74370875169
-Dl -0.364437270418 -0.738869127051
-Dl -0.356580601657 -0.732378043925
-Dl -0.349592402583 -0.724399574256
-Dl -0.343596689036 -0.715097789986
-Dl -0.338717476855 -0.70463676306
-Dl -0.335078781877 -0.693180565421
-Dl -0.332804619942 -0.680893269013
-Dl -0.332019006889 -0.667938945778
-Dl -0.332019006889 -0.624653084222
-Dl -0.332804618418 -0.611698760513
-Dl -0.335078776459 -0.599411462818
-Dl -0.338717466187 -0.587955263282
-Dl -0.34359667278 -0.577494234053
-Dl -0.349592381417 -0.568192447278
-Dl -0.356580577273 -0.560213975102
-Dl -0.364437245526 -0.553722889673
-Dl -0.373038371355 -0.548883263138
-Dl -0.382259939935 -0.545859167642
-Dl -0.391977936444 -0.544814675333
-Dl -0.693055256 -0.544814675333
-Dl -0.693055256 0.671999333333
-Dl -0.694042585976 0.684300999329
-Dl -0.69690207426 0.69596052674
-Dl -0.701479968386 0.706824163099
-Dl -0.70762251589 0.716738155943
-Dl -0.715175964306 0.725548752806
-Dl -0.723986561168 0.733102201221
-Dl -0.733900554012 0.739244748725
-Dl -0.744764190372 0.743822642852
-Dl -0.756423717782 0.746682131135
-Dl -0.768725383778 0.747669461111
-Dl -0.822912868889 0.747669461111
-Dl -0.835214534885 0.746682131135
-Dl -0.846874062295 0.743822642852
-Dl -0.857737698655 0.739244748725
-Dl -0.867651691499 0.733102201221
-Dl -0.876462288361 0.725548752806
-Dl -0.884015736777 0.716738155943
-Dl -0.890158284281 0.706824163099
-Dl -0.894736178407 0.69596052674
-Dl -0.897595666691 0.684300999329
-Dl -0.898582996667 0.671999333333
-Dl -0.898582996667 -0.544814675333
-Dl -1.19004122333 -0.544814675333
-Dl -1.19975921378 -0.545859167642
-Dl -1.20898078052 -0.548883263138
-Dl -1.2175819079 -0.553722889673
-Dl -1.22543858025 -0.560213975102
-Dl -1.23242678189 -0.568192447278
-Dl -1.23842249716 -0.577494234053
-Dl -1.24330171038 -0.587955263282
-Dl -1.2469404059 -0.599411462818
-Dl -1.24921456803 -0.611698760513
-Dl -1.25000018111 -0.624653084222
-Dl -1.25000018111 -0.667938945778
-Dl -1.24921456955 -0.680893269013
-Dl -1.24694041132 -0.693180565421
-Dl -1.24330172105 -0.70463676306
-Dl -1.23842251341 -0.715097789986
-Dl -1.23242680306 -0.724399574256
-Dl -1.22543860463 -0.732378043925
-Dl -1.21758193279 -0.738869127051
-Dl -1.2089808022 -0.74370875169
-Dl -1.19975922749 -0.746732845899
-Dl -1.19004122333 -0.747777337733
-Dl -0.391977936444 -0.747777337733
-Dl -0.391977936444 -0.747777337733
-$EndMODULE DT-Logo-Back
-$EndLIBRARY
\ No newline at end of file
diff --git a/examples/dt-logo.svg b/examples/dt-logo.svg
deleted file mode 100644
index 9987a91..0000000
--- a/examples/dt-logo.svg
+++ /dev/null
@@ -1,162 +0,0 @@
-
-
-
-
diff --git a/examples/svg2mod.svg b/examples/svg2mod.svg
new file mode 100644
index 0000000..4ab9c93
--- /dev/null
+++ b/examples/svg2mod.svg
@@ -0,0 +1,205 @@
+
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..d48d073
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[aliases]
+test=pytest
+
+[tool:pytest]
+addopts = --pylint --pylint-rcfile=.linter/required-linting.rc
+
+[metadata]
+license_files = LICENSE
\ No newline at end of file
diff --git a/setup.py b/setup.py
index d51ddd7..df03109 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+'''
+to install system wide use `pip install .`
+testing is done via `python setup.py test`
+'''
+import subprocess
import setuptools
@@ -13,39 +18,58 @@
with open('README.md') as readme_file:
readme = readme_file.read()
+tag = ""
+
+try:
+ ps = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT)
+ tag = ps.decode('utf-8').strip()
+ tag = tag.replace("-", ".dev", 1).replace("-", "+")
+except:
+ tag = "0.dev0"
+
requirements = [
+ "fonttools"
]
+setup_requirements = [
+ "pytest-runner", "pytest-pylint",
+]
test_requirements = [
- # TODO: put package test requirements here
+ "pytest", "pylint", "pyenchant",
]
setup(
name='svg2mod',
- version='0.1.0',
+ version=tag,
description="Convert an SVG file to a KiCad footprint.",
+ long_description_content_type='text/markdown',
long_description=readme,
- author='https://github.com/mtl',
+ author='https://github.com/svg2mod',
author_email='',
- url='https://github.com/mtl/svg2mod',
+ url='https://github.com/svg2mod/svg2mod',
packages=setuptools.find_packages(),
- entry_points={'console_scripts':['svg2mod = svg2mod.svg2mod:main']},
+ entry_points={'console_scripts':['svg2mod = svg2mod.cli:main']},
package_dir={'svg2mod':'svg2mod'},
include_package_data=True,
package_data={'kipart': ['*.gif', '*.png']},
scripts=[],
install_requires=requirements,
- license="CC0-1.0",
+ license="GPLv2+",
zip_safe=False,
- keywords='svg2mod, KiCAD',
+ keywords='svg2mod, KiCAD, inkscape',
classifiers=[
- 'Development Status :: 3 - Alpha',
+ 'Development Status :: 4 - Beta',
'Intended Audience :: Science/Research',
- 'License :: OSI Approved :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: Manufacturing',
+ 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
'Natural Language :: English',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
],
- test_suite='tests',
+ setup_requires=setup_requirements,
+ test_suite='test',
tests_require=test_requirements
)
diff --git a/svg2mod/__init__.py b/svg2mod/__init__.py
index e69de29..7b3e7ec 100644
--- a/svg2mod/__init__.py
+++ b/svg2mod/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''
+This module contains the necessary tools to convert from
+the svg objects provided from the svg2mod.svg module to
+KiCad file formats.
+This currently supports both the pretty format and
+the legacy mod format.
+'''
\ No newline at end of file
diff --git a/svg2mod/cli.py b/svg2mod/cli.py
new file mode 100644
index 0000000..1ace1c8
--- /dev/null
+++ b/svg2mod/cli.py
@@ -0,0 +1,344 @@
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''
+The command line interface for using svg2mod
+as a tool in a terminal.
+'''
+
+import argparse
+import logging
+import os
+import shlex
+import sys
+import traceback
+
+import svg2mod.coloredlogger as coloredlogger
+from svg2mod.coloredlogger import logger, unfiltered_logger
+from svg2mod import svg
+from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLatest,
+ Svg2ModExportLegacy, Svg2ModExportLegacyUpdater,
+ Svg2ModExportPretty)
+from svg2mod.importer import Svg2ModImport
+
+#----------------------------------------------------------------------------
+
+def main():
+ '''This function handles the scripting package calls.
+ It is setup to read the arguments from `get_arguments()`
+ then parse the target svg file and output all converted
+ objects into a kicad footprint module.
+ '''
+
+ args,_ = get_arguments()
+
+
+ # Setup root logger to use terminal colored outputs as well as stdout and stderr
+ coloredlogger.split_logger(logger)
+
+ if args.debug_print:
+ logger.setLevel(logging.DEBUG)
+ elif args.verbose_print:
+ logger.setLevel(logging.INFO)
+ else:
+ logger.setLevel(logging.WARNING)
+
+ if args.input_file_name_flag and not args.input_file_name:
+ args.input_file_name = args.input_file_name_flag
+
+ if args.list_fonts:
+ fonts = svg.Text.load_system_fonts()
+ unfiltered_logger.info("Font Name: list of supported styles.")
+ for font in fonts:
+ fnt_text = f" {font}:"
+ for styles in fonts[font]:
+ fnt_text += f" {styles},"
+ fnt_text = fnt_text.strip(",")
+ unfiltered_logger.info(fnt_text)
+ sys.exit(0)
+ if args.default_font:
+ svg.Text.default_font = args.default_font
+
+ pretty = args.format in ['pretty','latest']
+ use_mm = args.units == 'mm'
+
+ if pretty:
+
+ if not use_mm:
+ logger.critical("Error: decimal units only allowed with legacy output type")
+ sys.exit( -1 )
+
+ try:
+ # Import the SVG:
+ imported = Svg2ModImport(
+ args.input_file_name,
+ args.module_name,
+ args.module_value,
+ args.ignore_hidden,
+ args.force_layer
+ )
+
+ # Pick an output file name if none was provided:
+ if args.output_file_name is None:
+
+ args.output_file_name = os.path.splitext(
+ os.path.basename( args.input_file_name )
+ )[ 0 ]
+
+ # Append the correct file name extension if needed:
+ if pretty:
+ extension = ".kicad_mod"
+ else:
+ extension = ".mod"
+ if args.output_file_name[ - len( extension ) : ] != extension:
+ args.output_file_name += extension
+
+ # Create an exporter:
+ if pretty:
+ exported = (Svg2ModExportPretty if args.format == "pretty" else Svg2ModExportLatest)(
+ imported,
+ args.output_file_name,
+ args.center,
+ args.scale_factor,
+ args.precision,
+ dpi = args.dpi,
+ pads = args.convert_to_pads,
+ )
+
+ else:
+
+ # If the module file exists, try to read it:
+ exported = None
+ if os.path.isfile( args.output_file_name ):
+
+ try:
+ exported = Svg2ModExportLegacyUpdater(
+ imported,
+ args.output_file_name,
+ args.center,
+ args.scale_factor,
+ args.precision,
+ args.dpi,
+ )
+
+ except Exception as e:
+ raise e
+
+ # Write the module file:
+ if exported is None:
+ exported = Svg2ModExportLegacy(
+ imported,
+ args.output_file_name,
+ args.center,
+ args.scale_factor,
+ args.precision,
+ use_mm = use_mm,
+ dpi = args.dpi,
+ )
+
+ cmd_args = [os.path.basename(sys.argv[0])] + sys.argv[1:]
+ cmdline = ' '.join(shlex.quote(x) for x in cmd_args)
+
+ # Export the footprint:
+ exported.write(cmdline)
+ except Exception as e:
+ if args.debug_print:
+ traceback.print_exc()
+ else:
+ logger.critical(f'Unhandled exception (Exiting)\n {type(e).__name__}: {e} ')
+ exit(-1)
+
+#----------------------------------------------------------------------------
+
+def get_arguments():
+ ''' Return an instance of pythons argument parser
+ with all the command line functionalities arguments
+ '''
+
+ parser = argparse.ArgumentParser(
+ description = (
+ 'Convert Inkscape SVG drawings to KiCad footprint modules.'
+ )
+ )
+
+ mux = parser.add_mutually_exclusive_group(required=True)
+
+ mux.add_argument(
+ nargs="?",
+ type = str,
+ dest = 'input_file_name',
+ metavar = 'IN_FILENAME',
+ help = "Name of the SVG file",
+ )
+
+ mux.add_argument(
+ '-i', '--input-file',
+ type = str,
+ dest = 'input_file_name_flag',
+ metavar = 'FILENAME',
+ help = "Name of the SVG file, but specified with a flag.",
+ )
+
+ parser.add_argument(
+ '-o', '--output-file',
+ type = str,
+ dest = 'output_file_name',
+ metavar = 'FILENAME',
+ help = "Name of the module file",
+ )
+
+ parser.add_argument(
+ '-c', '--center',
+ dest = 'center',
+ action = 'store_const',
+ const = True,
+ help = "Center the module to the center of the bounding box",
+ default = False,
+ )
+
+ parser.add_argument(
+ '-P', '--convert-pads',
+ dest = 'convert_to_pads',
+ action = 'store_const',
+ const = True,
+ help = "Convert any artwork on Cu layers to pads",
+ default = False,
+ )
+
+ parser.add_argument(
+ '-v', '--verbose',
+ dest = 'verbose_print',
+ action = 'store_const',
+ const = True,
+ help = "Print more verbose messages",
+ default = False,
+ )
+
+ parser.add_argument(
+ '--debug',
+ dest = 'debug_print',
+ action = 'store_const',
+ const = True,
+ help = "Print debug level messages",
+ default = False,
+ )
+
+ parser.add_argument(
+ '-x', '--exclude-hidden',
+ dest = 'ignore_hidden',
+ action = 'store_const',
+ const = True,
+ help = "Do not export hidden objects",
+ default = False,
+ )
+
+ parser.add_argument(
+ '--force', '--force-layer',
+ type = str,
+ dest = 'force_layer',
+ metavar = 'LAYER',
+ help = "Force everything into the single provided layer",
+ default = None,
+ )
+
+ parser.add_argument(
+ '-d', '--dpi',
+ type = int,
+ dest = 'dpi',
+ metavar = 'DPI',
+ help = "DPI of the SVG file (int)",
+ default = DEFAULT_DPI,
+ )
+
+ parser.add_argument(
+ '-f', '--factor',
+ type = float,
+ dest = 'scale_factor',
+ metavar = 'FACTOR',
+ help = "Scale paths by this factor",
+ default = 1.0,
+ )
+
+ parser.add_argument(
+ '-p', '--precision',
+ type = float,
+ dest = 'precision',
+ metavar = 'PRECISION',
+ help = "Smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)",
+ default = 5.0,
+ )
+ parser.add_argument(
+ '--format',
+ type = str,
+ dest = 'format',
+ metavar = 'FORMAT',
+ choices = [ 'legacy', 'pretty', 'latest'],
+ help = "Output module file format (legacy|pretty|latest). 'latest' introduces features used in kicad >= 6",
+ default = 'latest',
+ )
+
+ parser.add_argument(
+ '--name', '--module-name',
+ type = str,
+ dest = 'module_name',
+ metavar = 'NAME',
+ help = "Base name of the module",
+ default = "svg2mod",
+ )
+
+ parser.add_argument(
+ '--units',
+ type = str,
+ dest = 'units',
+ metavar = 'UNITS',
+ choices = [ 'decimal', 'mm' ],
+ help = "Output units, if output format is legacy (decimal|mm)",
+ default = 'mm',
+ )
+
+ parser.add_argument(
+ '--value', '--module-value',
+ type = str,
+ dest = 'module_value',
+ metavar = 'VALUE',
+ help = "Value of the module",
+ default = "G***",
+ )
+
+ parser.add_argument(
+ '-F', '--default-font',
+ type = str,
+ dest = 'default_font',
+ help = "Default font to use if the target font in a text element cannot be found",
+ )
+
+ mux.add_argument(
+ '-l', '--list-fonts',
+ dest = 'list_fonts',
+ const = True,
+ default = False,
+ action = "store_const",
+ help = "List all fonts that can be found in common locations",
+ )
+
+ return parser.parse_args(), parser
+
+#----------------------------------------------------------------------------
+
+if __name__ == "__main__":
+ main()
+
+#----------------------------------------------------------------------------
diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py
new file mode 100644
index 0000000..5101e59
--- /dev/null
+++ b/svg2mod/coloredlogger.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+'''
+A simple modification to the formatter class in the python logger to allow
+ANSI color codes based on the logged message's level
+'''
+
+import logging
+import sys
+
+#----------------------------------------------------------------------------
+
+#Setup and configure svg2mod and svg2mod-unfiltered loggers
+
+logger = logging.getLogger("svg2mod")
+unfiltered_logger = logging.getLogger("svg2mod-unfiltered")
+_sh = logging.StreamHandler(sys.stdout)
+
+logger.addHandler(_sh)
+unfiltered_logger.addHandler(_sh)
+
+logger.setLevel(logging.DEBUG)
+
+# Add a second logger that will bypass the log level and output anyway
+# It is a good practice to send only messages level INFO via this logger
+unfiltered_logger.setLevel(logging.INFO)
+
+# This can be used sparingly as follows:
+#---------
+# unfiltered_logger.info("Message Here")
+#---------
+
+
+#----------------------------------------------------------------------------
+
+class Formatter(logging.Formatter):
+ '''Extend formatter to add colored output functionality '''
+
+ # ASCII escape codes for supporting terminals
+ color = {
+ logging.CRITICAL: "\033[91m\033[7m", #Set red and swap background and foreground
+ logging.ERROR: "\033[91m", #Set red
+ logging.WARNING: "\033[93m", #Set yellow
+ logging.DEBUG: "\033[90m", #Set dark gray/black
+ logging.INFO: "" #Do nothing
+ }
+ reset = "\033[0m" # Reset the terminal back to default color/emphasis
+
+ #------------------------------------------------------------------------
+
+ def __init__(self, fmt="%(message)s", datefmt=None, style="%"):
+ super().__init__(fmt, datefmt, style)
+
+ #------------------------------------------------------------------------
+
+ def format(self, record):
+ '''Overwrite the format function.
+ This saves the original style, overwrites it to support
+ color, sends the message to the super.format, and
+ finally returns the style to the original format
+ '''
+ if sys.stdout.isatty():
+ fmt_org = self._style._fmt
+ self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset
+ result = logging.Formatter.format(self, record)
+ if sys.stdout.isatty():
+ self._style._fmt = fmt_org
+ return result
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+
+def split_logger(logger, formatter=Formatter(), break_point=logging.WARNING):
+ '''This will split logging messages at the specified break point. Anything higher
+ will be sent to sys.stderr and everything else to sys.stdout
+ '''
+ for handler in logger.handlers:
+ logger.removeHandler(handler)
+
+ handler_error = logging.StreamHandler(sys.stderr)
+ handler_error.addFilter(lambda msg: break_point <= msg.levelno)
+
+ handler_out = logging.StreamHandler(sys.stdout)
+ handler_out.addFilter(lambda msg: break_point > msg.levelno)
+
+ handler_error.setFormatter(formatter)
+ handler_out.setFormatter(formatter)
+ logger.addHandler(handler_error)
+ logger.addHandler(handler_out)
+
+#----------------------------------------------------------------------------
\ No newline at end of file
diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py
new file mode 100644
index 0000000..5f78a53
--- /dev/null
+++ b/svg2mod/exporter.py
@@ -0,0 +1,1438 @@
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''
+Tools to convert data from Svg2ModImport to
+the file information used in kicad module files.
+'''
+
+
+import copy
+import datetime
+import io
+import json
+import os
+import re
+import time
+from abc import ABC, abstractmethod
+
+from svg2mod import svg
+from svg2mod.coloredlogger import logger, unfiltered_logger
+from svg2mod.importer import Svg2ModImport
+from svg2mod.svg2mod import PolygonSegment
+
+#----------------------------------------------------------------------------
+
+DEFAULT_DPI = 96 # 96 as of Inkscape 0.92
+MINIMUM_SIZE = 1e-5 # Minimum size kicad will render
+
+#----------------------------------------------------------------------------
+
+class Svg2ModExport(ABC):
+ ''' An abstract class to provide functionality
+ to write to kicad module file.
+ The abstract methods are the file type specific
+ example: pretty, legacy
+ '''
+
+ #------------------------------------------------------------------------
+
+ @property
+ @abstractmethod
+ def layer_map(self ):
+ ''' This should be overwritten by a dictionary object of layer maps '''
+ pass
+
+ @abstractmethod
+ def _get_layer_name( self, item_name, name, front ):pass
+
+ @abstractmethod
+ def _write_library_intro( self, cmdline ): pass
+
+ @abstractmethod
+ def _get_module_name( self, front = None ): pass
+
+ @abstractmethod
+ def _write_module_header( self, label_size, label_pen, reference_y, value_y, front,): pass
+
+ @abstractmethod
+ def _write_modules( self ): pass
+
+ @abstractmethod
+ def _write_module_footer( self, front ):pass
+
+ @abstractmethod
+ def _write_polygon_header( self, points, layer ):pass
+
+ @abstractmethod
+ def _write_polygon_footer( self, layer, stroke_width, fill=True):pass
+
+ @abstractmethod
+ def _write_polygon_point( self, point ):pass
+
+ @abstractmethod
+ def _write_polygon_segment( self, p, q, layer, stroke_width ):pass
+
+ @abstractmethod
+ def _write_thru_hole( self, circle, layer ):pass
+
+ #------------------------------------------------------------------------
+
+ @staticmethod
+ def _convert_decimal_to_mm( decimal ):
+ return float( decimal ) * 0.00254
+
+
+ #------------------------------------------------------------------------
+
+ @staticmethod
+ def _convert_mm_to_decimal( mm ):
+ return int( round( mm * 393.700787 ) )
+
+
+ #------------------------------------------------------------------------
+
+ def _get_fill_stroke( self, item ):
+
+ s = item.style
+
+ fill = False if not s.get('fill') or s["fill"] == "none" else True
+ fill = fill if not s.get('fill-opacity') or float(s['fill-opacity']) != 0 else False
+
+ stroke = False if not s.get('stroke') or s["stroke"] == "none" else True
+ stroke = stroke if not s.get('stroke-opacity') or float(s['stroke-opacity']) != 0 else False
+
+ stroke_width = s["stroke-width"] * self.scale_factor if s.get('stroke-width') else MINIMUM_SIZE
+
+ if stroke_width is None:
+ stroke_width = 0
+
+ # This should display something.
+ if not self.imported.ignore_hidden and not fill and not stroke:
+ stroke = True
+ stroke_width = stroke_width if stroke_width else MINIMUM_SIZE
+
+ # There should be no stroke_width if no stroke
+ return fill, stroke, stroke_width if stroke else 0
+
+
+ #------------------------------------------------------------------------
+
+ def __init__(
+ self,
+ svg2mod_import = Svg2ModImport(),
+ file_name = None,
+ center = False,
+ scale_factor = 1.0,
+ precision = 20.0,
+ use_mm = True,
+ dpi = DEFAULT_DPI,
+ pads = False,
+ ):
+ if use_mm:
+ # 25.4 mm/in;
+ scale_factor *= 25.4 / float(dpi)
+ use_mm = True
+ else:
+ # PCBNew uses decimal (10K DPI);
+ scale_factor *= 10000.0 / float(dpi)
+
+ self.imported = svg2mod_import
+ self.file_name = file_name
+ self.center = center
+ self.scale_factor = scale_factor
+ self.precision = precision
+ self.use_mm = use_mm
+ self.dpi = dpi
+ self.convert_pads = pads
+
+ # Local instance variables
+ self.translation = None
+ self.layers = {}
+ self.output_file = None
+ self.raw_file_data = None
+
+
+ #------------------------------------------------------------------------
+
+ def add_svg_element(self, elem : svg.Transformable, layer="F.SilkS"):
+ ''' This can be used to add a svg element
+ to a specific layer.
+ If the importer doesn't have a svg element
+ it will also create an empty Svg object.
+ '''
+ grp = svg.Group()
+ grp.name = layer
+ grp.items.append(elem)
+ try:
+ self.imported.svg.items.append(grp)
+ except AttributeError:
+ self.imported.svg = svg.Svg()
+ self.imported.svg.items.append(grp)
+
+ #------------------------------------------------------------------------
+
+
+ def _calculate_translation( self ):
+
+ min_point, max_point = self.imported.svg.bbox()
+
+ if self.center:
+ # Center the drawing:
+ adjust_x = min_point.x + ( max_point.x - min_point.x ) / 2.0
+ adjust_y = min_point.y + ( max_point.y - min_point.y ) / 2.0
+
+ self.translation = svg.Point(
+ 0.0 - adjust_x,
+ 0.0 - adjust_y,
+ )
+
+ else:
+ self.translation = svg.Point(
+ 0.0,
+ 0.0,
+ )
+
+ #------------------------------------------------------------------------
+
+ def _prune( self, items = None ):
+ '''Find and keep only the layers of interest.'''
+
+ empty_group_exception = items is None
+
+ if items is None:
+
+ self.layers = {}
+ for name in self.layer_map.keys():
+ self.layers[ name ] = []
+
+
+ items = self.imported.svg.items
+ self.imported.svg.items = []
+
+ kept_layers = {}
+
+ for item in items:
+
+ if not hasattr(item, 'name'):
+ continue
+
+ i_name = item.name.split(":", 1)
+
+ for name in self.layers.keys():
+ # if name == i_name[0] and i_name[0] != "":
+ if re.match( '^{}$'.format(name), i_name[0]):
+ # Don't add empty groups to the list of valid items
+ if isinstance(item, svg.Group) and not item.items:
+ break
+
+ if kept_layers.get(i_name[0]):
+ kept_layers[i_name[0]].append(item.name)
+ else:
+ kept_layers[i_name[0]] = [item.name]
+
+ # Item isn't a group so make it one
+ if not isinstance(item, svg.Group):
+ grp = svg.Group()
+ grp.name = item.name
+ grp.items.append( item )
+ item = grp
+
+ # save valid groups
+ self.imported.svg.items.append( item )
+ self.layers[name].append((i_name, item))
+ break
+ else:
+ self._prune( item.items )
+
+ for kept in sorted(kept_layers.keys()):
+ unfiltered_logger.info( "Found SVG layer: {}".format( kept ) )
+ logger.debug( " Detailed names: [{}]".format( ", ".join(kept_layers[kept]) ) )
+
+ # There are no elements to write so don't write
+ if empty_group_exception:
+ for name in self.layers:
+ if self.layers[name]:
+ break
+ else:
+ logger.warning("No valid items found. Maybe try --force Layer.Name")
+ raise Exception("Not writing empty file. No valid items found.")
+
+ #------------------------------------------------------------------------
+
+ def _write_items( self, items, layer, flip = False ):
+
+ for item in items:
+
+ if isinstance( item, svg.Group ):
+ self._write_items( item.items, layer, flip )
+ continue
+
+ if re.match(r"^Drill\.\w+", str(layer)):
+ if isinstance(item, (svg.Circle, svg.Ellipse)):
+ self._write_thru_hole(item, layer)
+ else:
+ logger.warning( "Non Circle SVG element in drill layer: {}".format(item.__class__.__name__))
+
+ elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text, svg.Polygon)):
+
+ segments = [
+ PolygonSegment( segment )
+ for segment in item.segments(
+ precision = self.precision
+ )
+ ]
+
+ fill, stroke, stroke_width = self._get_fill_stroke( item )
+ if layer == "Edge.Cuts":
+ fill = False
+ stroke = True
+ stroke_width = MINIMUM_SIZE if stroke_width < MINIMUM_SIZE else stroke_width
+
+
+ fill = (True if re.match("^Keepout", str(layer)) else fill)
+ stroke_width = (0.508 if re.match("^Keepout", str(layer)) else stroke_width)
+
+ for segment in segments:
+ segment.process( self, flip, fill )
+
+ if len( segments ) > 1:
+ # Sort segments in order of size
+ segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True)
+
+ # Write all segments
+ while len(segments) > 0:
+ inlinable = [segments[0]]
+
+ # Search to see if any paths are contained in the current shape
+ for seg in segments[1:]:
+ # Contained in parent shape
+ if fill and not inlinable[0].are_distinct(seg):
+ append = True
+ if len(inlinable) > 1:
+ for hole in inlinable[1:]:
+ # Contained in a hole. It is separate
+ if not hole.are_distinct(seg):
+ append = False
+ break
+ if append: inlinable.append(seg)
+ for poly in inlinable:
+ segments.pop(segments.index(poly))
+ if len(inlinable) > 1:
+ points = inlinable[ 0 ].inline( inlinable[ 1 : ] )
+ elif len(inlinable) > 0:
+ points = inlinable[ 0 ].points
+
+ logger.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) ))
+
+ self._write_polygon(
+ points, layer, fill, stroke, stroke_width
+ )
+ continue
+
+ if len( segments ) > 0:
+ points = segments[ 0 ].points
+
+ if len ( segments ) == 1:
+
+ logger.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) ))
+
+ self._write_polygon(
+ points, layer, fill, stroke, stroke_width
+ )
+ else:
+ logger.info( " Skipping {} with 0 points".format(item.__class__.__name__))
+
+ else:
+ logger.warning( "Unsupported SVG element: {}".format(item.__class__.__name__))
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module( self, front ):
+
+ module_name = self._get_module_name( front )
+
+ min_point, max_point = self.imported.svg.bbox()
+ min_point = self.transform_point( min_point, flip = False )
+ max_point = self.transform_point( max_point, flip = False )
+
+ label_offset = 1200
+ label_size = 600
+ label_pen = 120
+
+ if self.use_mm:
+ label_size = self._convert_decimal_to_mm( label_size )
+ label_pen = self._convert_decimal_to_mm( label_pen )
+ reference_y = min_point.y - self._convert_decimal_to_mm( label_offset )
+ value_y = max_point.y + self._convert_decimal_to_mm( label_offset )
+ else:
+ reference_y = min_point.y - label_offset
+ value_y = max_point.y + label_offset
+
+ self._write_module_header(
+ label_size, label_pen,
+ reference_y, value_y,
+ front,
+ )
+
+ for name, groups in self.layers.items():
+ for i_name, group in groups:
+
+ if group is None: continue
+
+ layer = self._get_layer_name( i_name, name, front )
+
+ self._write_items( group.items, layer, not front )
+
+ self._write_module_footer( front )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon( self, points, layer, fill, stroke, stroke_width ):
+
+ if fill and len(points) > 2:
+ self._write_polygon_filled(
+ points, layer, stroke_width
+ )
+ return
+
+ # Polygons with a fill and stroke are drawn with the filled polygon above
+ if stroke:
+ if len(points) == 1:
+ points.append(copy.copy(points[0]))
+
+ self._write_polygon_outline(
+ points, layer, stroke_width
+ )
+ return
+
+ if len(points) < 3:
+ logger.debug(" Not writing non-polygon with no stroke.")
+ else:
+ logger.debug(" Polygon has no stroke or fill. Skipping.")
+
+
+ #------------------------------------------------------------------------
+
+
+ def _write_polygon_filled( self, points, layer, stroke_width = 0.0 ):
+
+ self._write_polygon_header( points, layer )
+
+ for point in points:
+ self._write_polygon_point( point )
+
+ self._write_polygon_footer( layer, stroke_width )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_outline( self, points, layer, stroke_width ):
+
+ prior_point = None
+ for point in points:
+
+ if prior_point is not None:
+
+ self._write_polygon_segment(
+ prior_point, point, layer, stroke_width
+ )
+
+ prior_point = point
+
+
+ #------------------------------------------------------------------------
+
+ def transform_point( self, point, flip = False ):
+ ''' Transform provided point by this
+ classes scale factor.
+ '''
+
+ transformed_point = svg.Point(
+ ( point.x + self.translation.x ) * self.scale_factor,
+ ( point.y + self.translation.y ) * self.scale_factor,
+ )
+
+ if flip:
+ transformed_point.x *= -1
+
+ if self.use_mm:
+ transformed_point.x = transformed_point.x
+ transformed_point.y = transformed_point.y
+ else:
+ transformed_point.x = int( round( transformed_point.x ) )
+ transformed_point.y = int( round( transformed_point.y ) )
+
+ return transformed_point
+
+
+ #------------------------------------------------------------------------
+
+ def write( self, cmdline="scripting" ):
+ '''Write the kicad footprint file.
+ The value from the command line argument
+ is set in a comment in the header of the file.
+
+ If self.file_name is not null then this will
+ overwrite the target file with the data provided.
+ However if it is null then all data is written
+ to the string IO class (for same API as writing)
+ then dumped into self.raw_file_data before the
+ writer is closed.
+ '''
+
+ self._prune()
+
+ # Must come after pruning:
+ self._calculate_translation()
+
+ if self.file_name:
+ unfiltered_logger.info( "Writing module file: {}".format( self.file_name ) )
+ self.output_file = open( self.file_name, 'w' )
+ else:
+ self.output_file = io.StringIO()
+
+ self._write_library_intro(cmdline)
+
+ self._write_modules()
+
+ if self.file_name is None:
+ self.raw_file_data = self.output_file.getvalue()
+
+ self.output_file.close()
+ self.output_file = None
+
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+
+class Svg2ModExportLegacy( Svg2ModExport ):
+ ''' A child of Svg2ModExport that implements
+ specific functionality for kicad legacy file types
+ '''
+
+ layer_map = {
+ #'inkscape-name' : [ kicad-front, kicad-back ],
+ 'F.Cu' : [ 15, 15 ],
+ 'B.Cu' : [ 0, 0 ],
+ 'F.Adhes' : [ 17, 17 ],
+ 'B.Adhes' : [ 16, 16 ],
+ 'F.Paste' : [ 19, 19 ],
+ 'B.Paste' : [ 18, 18 ],
+ 'F.SilkS' : [ 21, 21 ],
+ 'B.SilkS' : [ 20, 20 ],
+ 'F.Mask' : [ 23, 23 ],
+ 'B.Mask' : [ 22, 22 ],
+ 'Dwgs.User' : [ 24, 24 ],
+ 'Cmts.User' : [ 25, 25 ],
+ 'Eco1.User' : [ 26, 26 ],
+ 'Eco2.User' : [ 27, 27 ],
+ 'Edge.Cuts' : [ 28, 28 ],
+ }
+
+
+ #------------------------------------------------------------------------
+
+ def __init__(
+ self,
+ svg2mod_import,
+ file_name,
+ center,
+ scale_factor = 1.0,
+ precision = 20.0,
+ use_mm = True,
+ dpi = DEFAULT_DPI,
+ ):
+ super( Svg2ModExportLegacy, self ).__init__(
+ svg2mod_import,
+ file_name,
+ center,
+ scale_factor,
+ precision,
+ use_mm,
+ dpi,
+ pads = False,
+ )
+
+ self.include_reverse = True
+
+
+ #------------------------------------------------------------------------
+
+ def _get_layer_name( self, item_name, name, front ):
+
+ layer_info = self.layer_map[ name ]
+ layer = layer_info[ 0 ]
+ if not front and layer_info[ 1 ] is not None:
+ layer = layer_info[ 1 ]
+
+ return layer
+
+
+ #------------------------------------------------------------------------
+
+ def _get_module_name( self, front = None ):
+
+ if self.include_reverse and not front:
+ return self.imported.module_name + "-rev"
+
+ return self.imported.module_name
+
+
+ #------------------------------------------------------------------------
+
+ def _write_library_intro( self, cmdline ):
+
+ modules_list = self._get_module_name( front = True )
+ if self.include_reverse:
+ modules_list += (
+ "\n" +
+ self._get_module_name( front = False )
+ )
+
+ units = ""
+ if self.use_mm:
+ units = "\nUnits mm"
+
+ self.output_file.write( """PCBNEW-LibModule-V1 {0}{1}
+$INDEX
+{2}
+$EndINDEX
+#
+# Converted using: {3}
+#
+""".format(
+ datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ),
+ units,
+ modules_list,
+ cmdline.replace("\\","\\\\")
+ )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_header(
+ self, label_size, label_pen,
+ reference_y, value_y, front,
+ ):
+
+ self.output_file.write( """$MODULE {0}
+Po 0 0 0 {6} 00000000 00000000 ~~
+Li {0}
+T0 0 {1} {2} {2} 0 {3} N I 21 "{0}"
+T1 0 {5} {2} {2} 0 {3} N I 21 "{4}"
+""".format(
+ self._get_module_name( front ), #0
+ reference_y, #1
+ label_size, #2
+ label_pen, #3
+ self.imported.module_value, #4
+ value_y, #5
+ 15, # Seems necessary #6
+ )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_footer( self, front ):
+
+ self.output_file.write(
+ "$EndMODULE {0}\n".format( self._get_module_name( front ) )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_modules( self ):
+
+ self._write_module( front = True )
+
+ if self.include_reverse:
+ self._write_module( front = False )
+
+ self.output_file.write( "$EndLIBRARY" )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_footer( self, layer, stroke_width, fill=True ):
+
+ pass
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_header( self, points, layer ):
+
+ pen = 1
+ if self.use_mm:
+ pen = self._convert_decimal_to_mm( pen )
+
+ self.output_file.write( "DP 0 0 0 0 {} {} {}\n".format(
+ len( points ),
+ pen,
+ layer
+ ) )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_point( self, point ):
+
+ self.output_file.write(
+ "Dl {} {}\n".format( point.x, point.y )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_segment( self, p, q, layer, stroke_width ):
+
+ self.output_file.write( "DS {} {} {} {} {} {}\n".format(
+ p.x, p.y,
+ q.x, q.y,
+ stroke_width,
+ layer
+ ) )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_thru_hole( self, circle, layer ):
+
+ pass
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+
+class Svg2ModExportLegacyUpdater( Svg2ModExportLegacy ):
+ ''' A Svg2Mod exporter class that reads some settings
+ from an already existing module and will append its
+ changes to the file.
+ '''
+
+ #------------------------------------------------------------------------
+
+ def __init__(
+ self,
+ svg2mod_import,
+ file_name,
+ center,
+ scale_factor = 1.0,
+ precision = 20.0,
+ dpi = DEFAULT_DPI,
+ include_reverse = True,
+ ):
+ self.file_name = file_name
+ use_mm = self._parse_output_file()
+
+ super( Svg2ModExportLegacyUpdater, self ).__init__(
+ svg2mod_import,
+ file_name,
+ center,
+ scale_factor,
+ precision,
+ use_mm,
+ dpi,
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _parse_output_file( self ):
+
+ logger.info( "Parsing module file: {}".format( self.file_name ) )
+ module_file = open( self.file_name, 'r' )
+ lines = module_file.readlines()
+ module_file.close()
+
+ self.loaded_modules = {}
+ self.post_index = []
+ self.pre_index = []
+ use_mm = False
+
+ index = 0
+
+ # Find the start of the index:
+ while index < len( lines ):
+
+ line = lines[ index ]
+ index += 1
+ self.pre_index.append( line )
+ if line[ : 6 ] == "$INDEX":
+ break
+
+ m = re.match( r"Units[\s]+mm[\s]*", line )
+ if m is not None:
+ use_mm = True
+
+ # Read the index:
+ while index < len( lines ):
+
+ line = lines[ index ]
+ if line[ : 9 ] == "$EndINDEX":
+ break
+ index += 1
+ self.loaded_modules[ line.strip() ] = []
+
+ # Read up until the first module:
+ while index < len( lines ):
+
+ line = lines[ index ]
+ if line[ : 7 ] == "$MODULE":
+ break
+ index += 1
+ self.post_index.append( line )
+
+ # Read modules:
+ while index < len( lines ):
+
+ line = lines[ index ]
+ if line[ : 7 ] == "$MODULE":
+ module_name, module_lines, index = self._read_module( lines, index )
+ if module_name is not None:
+ self.loaded_modules[ module_name ] = module_lines
+
+ elif line[ : 11 ] == "$EndLIBRARY":
+ break
+
+ else:
+ raise Exception(
+ "Expected $EndLIBRARY: [{}]".format( line )
+ )
+
+ return use_mm
+
+
+ #------------------------------------------------------------------------
+
+ def _read_module( self, lines, index ):
+
+ # Read module name:
+ m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] )
+ module_name = m.group( 1 )
+
+ logger.info( " Reading module {}".format( module_name ) )
+
+ index += 1
+ module_lines = []
+ while index < len( lines ):
+
+ line = lines[ index ]
+ index += 1
+
+ m = re.match(
+ r'\$EndMODULE[\s]+' + module_name + r'[\s]*', line
+ )
+ if m is not None:
+ return module_name, module_lines, index
+
+ module_lines.append( line )
+
+ raise Exception(
+ "Could not find end of module '{}'".format( module_name )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_library_intro( self, cmdline ):
+
+ # Write pre-index:
+ self.output_file.writelines( self.pre_index )
+
+ self.loaded_modules[ self._get_module_name( front = True ) ] = None
+ if self.include_reverse:
+ self.loaded_modules[
+ self._get_module_name( front = False )
+ ] = None
+
+ # Write index:
+ for module_name in sorted(
+ self.loaded_modules.keys(),
+ key = str.lower
+ ):
+ self.output_file.write( module_name + "\n" )
+
+ # Write post-index:
+ self.output_file.writelines( self.post_index )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_preserved_modules( self, up_to = None ):
+
+ if up_to is not None:
+ up_to = up_to.lower()
+
+ for module_name in sorted(
+ self.loaded_modules.keys(),
+ key = str.lower
+ ):
+ if up_to is not None and module_name.lower() >= up_to:
+ continue
+
+ module_lines = self.loaded_modules[ module_name ]
+
+ if module_lines is not None:
+
+ self.output_file.write(
+ "$MODULE {}\n".format( module_name )
+ )
+ self.output_file.writelines( module_lines )
+ self.output_file.write(
+ "$EndMODULE {}\n".format( module_name )
+ )
+
+ self.loaded_modules[ module_name ] = None
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_footer( self, front ):
+
+ super( Svg2ModExportLegacyUpdater, self )._write_module_footer(
+ front,
+ )
+
+ # Write remaining modules:
+ if not front:
+ self._write_preserved_modules()
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_header(
+ self,
+ label_size,
+ label_pen,
+ reference_y,
+ value_y,
+ front,
+ ):
+ self._write_preserved_modules(
+ up_to = self._get_module_name( front )
+ )
+
+ super( Svg2ModExportLegacyUpdater, self )._write_module_header(
+ label_size,
+ label_pen,
+ reference_y,
+ value_y,
+ front,
+ )
+
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+
+class Svg2ModExportPretty( Svg2ModExport ):
+ ''' This provides functionality for the
+ older kicad "pretty" footprint file formats.
+ It is a child of Svg2ModExport.
+ '''
+
+ layer_map = {
+ #'inkscape-name' : kicad-name,
+ 'F.Cu' : "F.Cu",
+ 'B.Cu' : "B.Cu",
+ 'F.Adhes' : "F.Adhes",
+ 'B.Adhes' : "B.Adhes",
+ 'F.Paste' : "F.Paste",
+ 'B.Paste' : "B.Paste",
+ 'F.SilkS' : "F.SilkS",
+ 'B.SilkS' : "B.SilkS",
+ 'F.Mask' : "F.Mask",
+ 'B.Mask' : "B.Mask",
+ 'Dwgs.User' : "Dwgs.User",
+ 'Cmts.User' : "Cmts.User",
+ 'Eco1.User' : "Eco1.User",
+ 'Eco2.User' : "Eco2.User",
+ 'Edge.Cuts' : "Edge.Cuts",
+ 'F.CrtYd' : "F.CrtYd",
+ 'B.CrtYd' : "B.CrtYd",
+ 'F.Fab' : "F.Fab",
+ 'B.Fab' : "B.Fab",
+ 'Drill.Cu': "Drill.Cu",
+ 'Drill.Mech': "Drill.Mech",
+ }
+
+ keepout_allowed = ['tracks','vias','pads','copperpour','footprints']
+
+
+ # Breaking changes where introduced in kicad v6
+ # This variable disables the drill breaking changes for v5 support
+ _drill_inner_layers = False
+
+
+ #------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super(Svg2ModExportPretty, self).__init__(*args, **kwargs)
+ self._special_footer = ""
+ self._extra_indent = 0
+
+ #------------------------------------------------------------------------
+
+ def _get_layer_name( self, item_name, name, front ):
+
+ name = self.layer_map[ name ]
+
+ # For pretty format layers can have attributes in the svg name
+ # This validates all the layers and converts them to a json string
+ # which is attached to the returned value with `:`
+ attrs = {}
+
+ # Keepout layer validation and expansion
+ if name == "Keepout":
+ attrs["layers"] = []
+ layers = re.match(r'^(.*)\.Keepout', item_name[0]).groups()[0]
+ if len(layers) == 1 and layers not in "BFI*":
+ raise Exception("Unexpected keepout layer: {} in {}".format(layers, item_name[0]))
+ if len(layers) == 1:
+ if layers == '*':
+ attrs['layers'] = ['F','B','I']
+ else:
+ attrs['layers'] = [layers]
+ else:
+ for layer in layers:
+ if layer not in "FBI":
+ raise Exception("Unexpected keepout layer: {} in {}".format(layer, item_name[0]))
+ if layer != '&':
+ attrs['layers'].append(layer)
+
+ # All attributes with the exception of keepout layers is validated
+ if len(item_name) == 2 and item_name[1]:
+ for arg in item_name[1].split(';'):
+ arg = arg.strip(' ,:')
+
+ # This is used in Svg2ModExportLatest as it is a breaking change
+ # Keepout allowed items
+ if name == "Keepout" and re.match(r'^allowed:\w+', arg, re.I):
+ attrs["allowed"] = []
+ for allowed in arg.lower().split(":", 1)[1].split(','):
+ if allowed in self.keepout_allowed:
+ attrs["allowed"].append(allowed)
+ else:
+ logger.warning("Invalid allowed option in keepout: {} in {}".format(allowed, arg))
+ # Zone hatch patterns
+ elif name == "Keepout" and re.match(r'^hatch:(none|edge|full)$', arg, re.I):
+ attrs["hatch"] = arg.split(":", 1)[1]
+
+ #Copper pad attributes
+ elif re.match(r'^\w+\.Cu', name) and re.match(r'^pad(:(\d+|mask|paste))?', arg, re.I):
+ if arg.lower() == "pad":
+ attrs["copper_pad"] = True
+ else:
+ ops = arg.split(":", 1)[1]
+ for opt in ops.split(','):
+ if re.match(r'^\d+$', opt):
+ attrs["copper_pad"] = int(opt)
+ elif opt.lower() == "mask" and name != "Drill.Cu":
+ attrs["pad_mask"] = True
+ if not attrs.get("copper_pad"):
+ attrs["copper_pad"] = True
+ elif opt.lower() == "paste" and name != "Drill.Cu":
+ attrs["pad_paste"] = True
+ if not attrs.get("copper_pad"):
+ attrs["copper_pad"] = True
+ else:
+ logger.warning("Invalid pad option '{}' for layer {}".format(opt, name))
+ else:
+ logger.warning("Unexpected option: {} for {}".format(arg, item_name[0]))
+ if attrs:
+ return name+":"+json.dumps(attrs)
+ return name
+
+
+ #------------------------------------------------------------------------
+
+ def _get_module_name( self, front = None ):
+
+ return self.imported.module_name
+
+
+ #------------------------------------------------------------------------
+
+ def _write_library_intro( self, cmdline ):
+
+ self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X})
+ (attr virtual)
+ (descr "{2}")
+ (tags {3})
+""".format(
+ self.imported.module_name, #0
+ int( round( #1
+ os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time()
+ ) ),
+ "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2
+ "svg2mod", #3
+ )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_footer( self, front ):
+
+ self.output_file.write( "\n)" )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_module_header(
+ self, label_size, label_pen,
+ reference_y, value_y, front,
+ ):
+ if front:
+ side = "F"
+ else:
+ side = "B"
+
+ self.output_file.write(
+""" (fp_text reference {0} (at 0 {1}) (layer {2}.SilkS) hide
+ (effects (font (size {3} {3}) (thickness {4})))
+ )
+ (fp_text value {5} (at 0 {6}) (layer {2}.SilkS) hide
+ (effects (font (size {3} {3}) (thickness {4})))
+ )""".format(
+
+ self._get_module_name(), #0
+ reference_y, #1
+ side, #2
+ label_size, #3
+ label_pen, #4
+ self.imported.module_value, #5
+ value_y, #6
+ )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_modules( self ):
+
+ self._write_module( front = True )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_filled( self, points, layer, stroke_width = 0):
+ self._write_polygon_header( points, layer, stroke_width)
+
+ for point in points:
+ self._write_polygon_point( point )
+
+ self._write_polygon_footer( layer, stroke_width )
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_footer( self, layer, stroke_width, fill=True ):
+
+ #Format option #2 is expected, but only used in Svg2ModExportLatest
+ if self._special_footer:
+ self.output_file.write(self._special_footer.format(
+ layer.split(":", 1)[0], stroke_width, "" #2
+ ))
+ else:
+ self.output_file.write(
+ " )\n (layer {})\n (width {}){}\n )".format(
+ layer.split(":", 1)[0], stroke_width, "" #3
+ )
+ )
+ self._special_footer = ""
+ self._extra_indent = 0
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_header( self, points, layer, stroke_width):
+
+ l_name = layer
+ options = {}
+ try:
+ l_name, options = layer.split(":", 1)
+ options = json.loads(options)
+ except ValueError:pass
+
+ create_pad = (self.convert_pads and l_name.find("Cu") == 2) or options.get("copper_pad")
+
+ if stroke_width == 0:
+ stroke_width = MINIMUM_SIZE
+
+ if l_name == "Keepout":
+ self._extra_indent = 1
+ layers = ["*"]
+ if len(options["layers"]) == 3:
+ layers = ["*"]
+ elif "I" not in options["layers"]:
+ layers = ["&".join(options["layers"])]
+ else:
+ options["layers"].remove("I")
+ layers = options["layers"][:] + ['In{}'.format(i) for i in range(1,31)]
+
+
+ self.output_file.write( '''\n (zone (net 0) (net_name "") (layers "{0}.Cu") (hatch {1} {2:.6f})
+ (connect_pads (clearance 0))
+ (min_thickness {3:.6f})
+ (keepout ({4}allowed))
+ (fill (thermal_gap {2:.6f}) (thermal_bridge_width {2:.6f}))
+ (polygon
+ (pts\n'''.format(
+ '.Cu" "'.join(layers), #0
+ options["hatch"] if options.get("hatch") else "full", #1
+ stroke_width, #2
+ stroke_width/2, #3
+ "allowed) (".join(
+ [i+" "+(
+ "not_" if not options.get("allowed") or i not in options["allowed"] else ""
+ ) for i in self.keepout_allowed]
+ ), #4
+ )
+ )
+ self._special_footer = " )\n )\n )"
+ elif create_pad:
+ pad_number = "" if not options.get("copper_pad") or isinstance(options["copper_pad"], bool) else str(options.get("copper_pad"))
+ layer = l_name
+ if options.get("pad_mask"):
+ layer += " {}.Mask".format(l_name.split(".", 1)[0])
+ if options.get("pad_paste"):
+ layer += " {}.Paste".format(l_name.split(".", 1)[0])
+ self._extra_indent = 1
+
+ self._special_footer = "\n )"
+
+ self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4})
+ (zone_connect 0)
+ (options (clearance outline) (anchor circle))'''.format(
+ pad_number, #0
+ points[0].x, #1
+ points[0].y, #2
+ stroke_width, #3
+ layer, #4
+ )
+ )
+ # Pads primitives with 2 or less points crash kicad
+ if len(points) >= 2:
+ self.output_file.write('''\n (primitives\n (gr_poly (pts \n''')
+ self._special_footer = " )\n (width {}){{2}})\n ))".format(stroke_width)
+
+ origin_x = points[0].x
+ origin_y = points[0].y
+ for point in points:
+ point.x = point.x-origin_x
+ point.y = point.y-origin_y
+ else:
+ for point in points[:]:
+ points.remove(point)
+ else:
+ self.output_file.write( "\n (fp_poly\n (pts \n" )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_point( self, point ):
+
+ if self._extra_indent:
+ self.output_file.write(" "*self._extra_indent)
+
+ self.output_file.write(
+ " (xy {} {})\n".format( point.x, point.y )
+ )
+
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_segment( self, p, q, layer, stroke_width ):
+ l_name = layer
+ options = {}
+ try:
+ l_name, options = layer.split(":", 1)
+ options = json.loads(options)
+ except ValueError:pass
+
+ create_pad = (self.convert_pads and l_name.find("Cu") == 2) or options.get("copper_pad")
+
+ if stroke_width == 0:
+ stroke_width = MINIMUM_SIZE
+
+ if create_pad:
+ pad_number = "" if not options.get("copper_pad") or isinstance(options["copper_pad"], bool) else str(options.get("copper_pad"))
+ layer = l_name
+ if options.get("pad_mask"):
+ layer += " {}.Mask".format(l_name.split(".", 1)[0])
+ if options.get("pad_paste"):
+ layer += " {}.Paste".format(l_name.split(".", 1)[0])
+
+ # There are major performance issues when multiple line primitives are in the same pad
+ self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4})
+ (zone_connect 0)
+ (options (clearance outline) (anchor circle))
+ (primitives\n (gr_line (start 0 0) (end {5} {6}) (width {3}))
+ ))'''.format(
+ pad_number, #0
+ p.x, #1
+ p.y, #2
+ stroke_width, #3
+ layer, #4
+ q.x - p.x, #5
+ q.y - p.y, #6
+ )
+ )
+ else:
+
+ self.output_file.write(
+ """\n (fp_line
+ (start {} {}) (end {} {})
+ (layer {}) (width {})
+ )""".format(
+ p.x, p.y,
+ q.x, q.y,
+ layer.split(':',1)[0],
+ stroke_width,
+ )
+ )
+
+ #------------------------------------------------------------------------
+
+ def _write_thru_hole( self, circle, layer ):
+
+ if not isinstance(circle, svg.Circle):
+ logger.info("Found an ellipse in Drill layer. Using an average of rx and ry.")
+ circle.rx = (circle.rx + circle.ry ) / 2
+
+ l_name = layer
+ options = {}
+ try:
+ l_name, options = layer.split(":", 1)
+ options = json.loads(options)
+ except ValueError:pass
+
+ plated = l_name == "Drill.Cu"
+ pad_number = ""
+ if plated and options.get("copper_pad") and not isinstance(options["copper_pad"], bool):
+ pad_number = str(options.get("copper_pad"))
+
+ rad = circle.rx * self.scale_factor
+ drill = rad * 2
+
+ size = circle.style.get("stroke-width") * self.scale_factor
+ if size and plated:
+ drill -= size
+ size = size + ( rad * 2 )
+ elif size:
+ size = size + ( rad * 2 )
+ drill = size
+ else:
+ size = rad
+
+ center = self.transform_point(circle.center)
+
+ self.output_file.write(
+ '\n (pad "{0}" {1}thru_hole circle (at {2} {3}) (size {4} {4}) (drill {5}) (layers *.Mask{6}) {7})'.format(
+ pad_number, #0
+ "" if plated else "np_", #1
+ center.x, #2
+ center.y, #3
+ size, #4
+ drill, #5
+ " *.Cu" if plated else "", #6
+ "(remove_unused_layers) (keep_end_layers)" if plated and self._drill_inner_layers else "", #7
+ )
+ )
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
+
+
+class Svg2ModExportLatest(Svg2ModExportPretty):
+ ''' This provides functionality for the newer kicad
+ "pretty" footprint file formats introduced in kicad v6.
+ It is a child of Svg2ModExport.
+ '''
+
+ layer_map = {
+ #'inkscape-name' : kicad-name,
+ 'F.Cu' : "F.Cu",
+ 'B.Cu' : "B.Cu",
+ 'F.Adhes' : "F.Adhes",
+ 'B.Adhes' : "B.Adhes",
+ 'F.Paste' : "F.Paste",
+ 'B.Paste' : "B.Paste",
+ 'F.SilkS' : "F.SilkS",
+ 'B.SilkS' : "B.SilkS",
+ 'F.Mask' : "F.Mask",
+ 'B.Mask' : "B.Mask",
+ 'Dwgs.User' : "Dwgs.User",
+ 'Cmts.User' : "Cmts.User",
+ 'Eco1.User' : "Eco1.User",
+ 'Eco2.User' : "Eco2.User",
+ 'Edge.Cuts' : "Edge.Cuts",
+ 'F.CrtYd' : "F.CrtYd",
+ 'B.CrtYd' : "B.CrtYd",
+ 'F.Fab' : "F.Fab",
+ 'B.Fab' : "B.Fab",
+ 'Drill.Cu': "Drill.Cu",
+ 'Drill.Mech': "Drill.Mech",
+ r'\S+\.Keepout': "Keepout"
+ }
+
+ # Breaking changes where introduced in kicad v6
+ # This variable enables the drill breaking changes for v5 support
+ _drill_inner_layers = True
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_outline( self, points, layer, stroke_width = 0):
+ self._write_polygon_header( points, layer, stroke_width)
+
+ for point in points:
+ self._write_polygon_point( point )
+
+ self._write_polygon_footer( layer, stroke_width, fill=False )
+
+ #------------------------------------------------------------------------
+
+ def _write_polygon_footer( self, layer, stroke_width, fill=True ):
+
+ if self._special_footer:
+ self.output_file.write(self._special_footer.format(
+ layer.split(":", 1)[0], stroke_width,
+ " (fill none)" if not fill else ""
+ ))
+ else:
+ self.output_file.write(
+ " )\n (layer {})\n (width {}){}\n )".format(
+ layer.split(":", 1)[0], stroke_width,
+ " (fill none)" if not fill else ""
+ )
+ )
+ self._special_footer = ""
+ self._extra_indent = 0
+
+
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
diff --git a/svg2mod/importer.py b/svg2mod/importer.py
new file mode 100644
index 0000000..d641dca
--- /dev/null
+++ b/svg2mod/importer.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''
+Svg2ModImport is responsible for basic parsing
+of svg layers to be used with an instance of
+Svg2ModExport.
+'''
+
+
+from svg2mod import svg
+from svg2mod.coloredlogger import logger, unfiltered_logger
+
+#----------------------------------------------------------------------------
+
+class Svg2ModImport:
+ ''' An importer class to read in target svg,
+ parse it, and keep only layers on interest.
+ '''
+
+ #------------------------------------------------------------------------
+
+ def _prune_hidden( self, items = None ):
+
+ if items is None:
+
+ items = self.svg.items
+
+ for item in items[:]:
+
+ if hasattr(item, "hidden") and item.hidden:
+ if hasattr(item, "name") and item.name:
+ logger.warning("Ignoring hidden SVG item: {}".format( item.name ) )
+ items.remove(item)
+
+ if hasattr(item, "items") and item.items:
+ self._prune_hidden( item.items )
+
+ #------------------------------------------------------------------------
+
+ def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden=False, force_layer=None):
+
+ self.file_name = file_name
+ self.module_name = module_name
+ self.module_value = module_value
+ self.ignore_hidden = ignore_hidden
+
+ if file_name:
+ unfiltered_logger.info( "Parsing SVG..." )
+
+ self.svg = svg.parse( file_name )
+ logger.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale))
+ if force_layer:
+ new_layer = svg.Group()
+ new_layer.name = force_layer
+ new_layer.items = self.svg.items[:]
+ self.svg.items = [new_layer]
+ if self.ignore_hidden:
+ self._prune_hidden()
+
+
+ #------------------------------------------------------------------------
+
+#----------------------------------------------------------------------------
diff --git a/svg2mod/svg/LICENSE b/svg2mod/svg/LICENSE
deleted file mode 100644
index d159169..0000000
--- a/svg2mod/svg/LICENSE
+++ /dev/null
@@ -1,339 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along
- with this program; if not, write to the Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- , 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md
deleted file mode 100644
index 6ab06cd..0000000
--- a/svg2mod/svg/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-SVG parser library
-==================
-
-This is a SVG parser library written in Python.
-([see here](https://github.com/cjlano/svg]))
-
-Capabilities:
- - Parse SVG XML
- - apply any transformation (svg transform)
- - Explode SVG Path into basic elements (Line, Bezier, ...)
- - Interpolate SVG Path as a series of segments
- - Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm
-
-Not (yet) supported:
- - SVG Path Arc ('A')
- - Non-linear transformation drawing (SkewX, ...)
-
-License: GPLv2+
diff --git a/svg2mod/svg/__init__.py b/svg2mod/svg/__init__.py
index b3c8618..6d6bf0c 100644
--- a/svg2mod/svg/__init__.py
+++ b/svg2mod/svg/__init__.py
@@ -1 +1,11 @@
+'''
+A SVG parser with tools to convert an XML svg file
+to objects that can be simplified into points.
+'''
+#__all__ = ['geometry', 'svg']
+
from .svg import *
+
+def parse(filename):
+ '''Take in a filename and return a SVG object of parsed file'''
+ return Svg(filename)
diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/geometry.py
similarity index 80%
rename from svg2mod/svg/svg/geometry.py
rename to svg2mod/svg/geometry.py
index 7a4114f..a931281 100644
--- a/svg2mod/svg/svg/geometry.py
+++ b/svg2mod/svg/geometry.py
@@ -1,4 +1,5 @@
# Copyright (C) 2013 -- CJlano < cjlano @ free.fr >
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -24,6 +25,7 @@
import operator
class Point:
+ '''Define a point as two floats accessible by x and y'''
def __init__(self, x=None, y=None):
'''A Point is defined either by a tuple/list of length 2 or
by 2 coordinates
@@ -64,7 +66,7 @@ def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __sub__(self, other):
- '''Substract two Points.
+ '''Subtract two Points.
>>> Point(1,2) - Point(3,2)
(-2.000,0.000)
'''
@@ -104,7 +106,7 @@ def __repr__(self):
return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')'
def __str__(self):
- return self.__repr__();
+ return self.__repr__()
def coord(self):
'''Return the point tuple (x,y)'''
@@ -114,14 +116,21 @@ def length(self):
'''Vector length, Pythagoras theorem'''
return math.sqrt(self.x ** 2 + self.y ** 2)
- def rot(self, angle):
+ def rot(self, angle, x=0, y=0):
'''Rotate vector [Origin,self] '''
if not isinstance(angle, Angle):
try: angle = Angle(angle)
except: return NotImplemented
- x = self.x * angle.cos - self.y * angle.sin
- y = self.x * angle.sin + self.y * angle.cos
- return Point(x,y)
+ if angle.angle % (2 * math.pi) == 0:
+ return Point(self.x,self.y)
+
+ new_x = ((self.x-x) * angle.cos) - ((self.y-y) * angle.sin) + x
+ new_y = ((self.x-x) * angle.sin) + ((self.y-y) * angle.cos) + y
+ return Point(new_x,new_y)
+
+ def round(self, num_digits=None):
+ '''Round x and y to number of decimal points'''
+ return Point( round(self.x, num_digits), round(self.y, num_digits))
class Angle:
@@ -150,6 +159,11 @@ def __init__(self, arg):
def __neg__(self):
return Angle(Point(self.cos, -self.sin))
+ def __add__(self, other):
+ if not isinstance(other, Angle):
+ try: other = Angle(other)
+ except: return NotImplemented
+ return Angle(self.angle+other.angle)
class Segment:
'''A segment is an object defined by 2 points'''
@@ -160,7 +174,7 @@ def __init__(self, start, end):
def __str__(self):
return 'Segment from ' + str(self.start) + ' to ' + str(self.end)
- def segments(self, precision=0):
+ def segments(self, __=0):
''' Segments is simply the segment start -> end'''
return [self.start, self.end]
@@ -182,15 +196,15 @@ def pdistance(self, p):
if s.x == 0:
# Vertical Segment => pdistance is the difference of abscissa
return abs(self.start.x - p.x)
- else:
- # That's 2-D perpendicular distance formulae (ref: Wikipedia)
- slope = s.y/s.x
- # intercept: Crossing with ordinate y-axis
- intercept = self.start.y - (slope * self.start.x)
- return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1)
+ # That's 2-D perpendicular distance formula (ref: Wikipedia)
+ slope = s.y/s.x
+ # intercept: Crossing with ordinate y-axis
+ intercept = self.start.y - (slope * self.start.x)
+ return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1)
def bbox(self):
+ '''Return bounding box as ( Point(min), Point(max )'''
xmin = min(self.start.x, self.end.x)
xmax = max(self.start.x, self.end.x)
ymin = min(self.start.y, self.end.y)
@@ -199,19 +213,10 @@ def bbox(self):
return (Point(xmin,ymin),Point(xmax,ymax))
def transform(self, matrix):
+ '''Transform start and end point by provided matrix'''
self.start = matrix * self.start
self.end = matrix * self.end
- def scale(self, ratio):
- self.start *= ratio
- self.end *= ratio
- def translate(self, offset):
- self.start += offset
- self.end += offset
- def rotate(self, angle):
- self.start = self.start.rot(angle)
- self.end = self.end.rot(angle)
-
class Bezier:
'''Bezier curve class
A Bezier curve is defined by its control points
@@ -227,12 +232,12 @@ def __str__(self):
' : ' + ", ".join([str(x) for x in self.pts])
def control_point(self, n):
+ '''Return Point at index n'''
if n >= self.dimension:
raise LookupError('Index is larger than Bezier curve dimension')
- else:
- return self.pts[n]
+ return self.pts[n]
- def rlength(self):
+ def r_length(self):
'''Rough Bezier length: length of control point segments'''
pts = list(self.pts)
l = 0.0
@@ -244,9 +249,10 @@ def rlength(self):
return l
def bbox(self):
- return self.rbbox()
+ '''This returns the rough bounding box '''
+ return self.r_bbox()
- def rbbox(self):
+ def r_bbox(self):
'''Rough bounding box: return the bounding box (P1,P2) of the Bezier
_control_ points'''
xmin = min([p.x for p in self.pts])
@@ -257,12 +263,12 @@ def rbbox(self):
return (Point(xmin,ymin), Point(xmax,ymax))
def segments(self, precision=0):
- '''Return a polyline approximation ("segments") of the Bezier curve
- precision is the minimum significative length of a segment'''
+ '''Return a poly-line approximation ("segments") of the Bezier curve
+ precision is the minimum significant length of a segment'''
segments = []
# n is the number of Bezier points to draw according to precision
if precision != 0:
- n = int(self.rlength() / precision) + 1
+ n = int(self.r_length() / precision) + 1
else:
n = 1000
#if n < 10: n = 10
@@ -272,7 +278,8 @@ def segments(self, precision=0):
segments.append(self._bezierN(float(t)/n))
return segments
- def _bezier1(self, p0, p1, t):
+ @staticmethod
+ def _bezier1(p0, p1, t):
'''Bezier curve, one dimension
Compute the Point corresponding to a linear Bezier curve between
p0 and p1 at "time" t '''
@@ -291,36 +298,29 @@ def _bezierN(self, t):
# For each control point of nth dimension,
# compute linear Bezier point a t
for i in range(0,n-1):
- res[i] = self._bezier1(res[i], res[i+1], t)
+ res[i] = Bezier._bezier1(res[i], res[i+1], t)
return res[0]
def transform(self, matrix):
+ '''Transform every point by the provided matrix'''
self.pts = [matrix * x for x in self.pts]
- def scale(self, ratio):
- self.pts = [x * ratio for x in self.pts]
- def translate(self, offset):
- self.pts = [x + offset for x in self.pts]
- def rotate(self, angle):
- self.pts = [x.rot(angle) for x in self.pts]
-
class MoveTo:
+ '''MoveTo class
+ This will create a move without creating a segment
+ to the destination point.
+ '''
def __init__(self, dest):
self.dest = dest
def bbox(self):
+ '''This returns a single point bounding box. ( Point(destination), Point(destination) )'''
return (self.dest, self.dest)
def transform(self, matrix):
+ '''Transform the destination point by provided matrix'''
self.dest = matrix * self.dest
- def scale(self, ratio):
- self.dest *= ratio
- def translate(self, offset):
- self.dest += offset
- def rotate(self, angle):
- self.dest = self.dest.rot(angle)
-
def simplify_segment(segment, epsilon):
'''Ramer-Douglas-Peucker algorithm'''
@@ -334,10 +334,9 @@ def simplify_segment(segment, epsilon):
key=operator.itemgetter(1))
if maxDist > epsilon:
- # Recursively call with segment splited in 2 on its furthest point
+ # Recursively call with segment split in 2 on its furthest point
r1 = simplify_segment(segment[:index+1], epsilon)
r2 = simplify_segment(segment[index:], epsilon)
# Remove redundant 'middle' Point
return r1[:-1] + r2
- else:
- return [segment[0], segment[-1]]
+ return [segment[0], segment[-1]]
diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py
new file mode 100644
index 0000000..baa4d43
--- /dev/null
+++ b/svg2mod/svg/svg.py
@@ -0,0 +1,1491 @@
+# Copyright (C) 2013 -- CJlano < cjlano @ free.fr >
+# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod >
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+'''
+A SVG parser with tools to convert an XML svg file
+to objects that can be simplified into points.
+'''
+
+import copy
+import inspect
+import itertools
+import json
+import logging
+import math
+import operator
+import os
+import platform
+import re
+import sys
+import xml.etree.ElementTree as etree
+from typing import Iterable, List, Tuple
+
+from fontTools.misc import loggingTools
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.ttLib import ttFont
+
+from svg2mod.coloredlogger import logger
+
+from .geometry import Angle, Bezier, MoveTo, Point, Segment, simplify_segment
+
+svg_ns = '{http://www.w3.org/2000/svg}'
+
+# Regex commonly used
+number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
+unit_re = r'em|ex|px|in|cm|mm|pt|pc|%'
+
+# styles of interest and their defaults
+svg_defaults = {
+ "fill":"black",
+ "fill-opacity":"1",
+ "stroke":"none",
+ "stroke-width":"1px",
+ "stroke-opacity":"1",
+ }
+
+# Unit converter
+unit_convert = {
+ None: 1, # Default unit (same as pixel)
+ 'px': 1, # px: pixel. Default SVG unit
+ 'em': 10, # 1 em = 10 px FIXME
+ 'ex': 5, # 1 ex = 5 px FIXME
+ 'in': 96, # 1 in = 96 px
+ 'cm': 96 / 2.54, # 1 cm = 1/2.54 in
+ 'mm': 96 / 25.4, # 1 mm = 1/25.4 in
+ 'pt': 96 / 72.0, # 1 pt = 1/72 in
+ 'pc': 96 / 6.0, # 1 pc = 1/6 in
+ '%' : 1 / 100.0 # 1 percent
+ }
+
+# Logging spammed 'Unable to find font because no font was specified.'
+# this allows it to only print the error once before muting it for that run.
+_font_warning_sent = False
+
+
+class Transformable:
+ '''Abstract class for objects that can be geometrically drawn & transformed'''
+
+ # This list is all styles that should have the transformation matrix applied
+ transformable_styles = ["stroke-width"]
+
+ def __init__(self, elt=None, parent_styles=None):
+ # a 'Transformable' is represented as a list of Transformable items
+ self.items = []
+ self.id = hex(id(self))
+ self.name = ""
+ self.fill_even_odd = False
+ # Unit transformation matrix on init
+ self.matrix = Matrix()
+ self.xscale = 1
+ self.yscale = 1
+ self.style = svg_defaults.copy() if not parent_styles and not isinstance(parent_styles, dict) else parent_styles.copy()
+ self.rotation = 0
+ self.viewport = Point(800, 600) # default viewport is 800x600
+ if elt is not None:
+ self.id = elt.get('id', self.id)
+
+ # get inkscape:label as self.name
+ for ident, value in elt.attrib.items():
+
+ ident = self.parse_name( ident )
+ if ident[ "name" ] == "label":
+ self.name = value
+ break
+ # self.name isn't set so try setting name to id
+ if self.name == '':
+ self.name == self.id
+
+ # set fill_even_odd if property set
+ self.fill_even_odd = elt.get("fill-rule", '').lower() == 'evenodd'
+ if self.fill_even_odd:
+ logger.warning(f"Found unsupported attribute: 'fill-rule=evenodd' for {repr(self)}")
+
+ # Find attributes of interest. The are overwritten by styles
+ for style_key in svg_defaults:
+ self.style[style_key] = elt.get(style_key, self.style[style_key])
+
+ # parse styles and save as dictionary.
+ if elt.get('style'):
+ for style in elt.get('style').split(";"):
+ if style.find(":") == -1:
+ self.style[name] = None
+ else:
+ nv = style.split(":")
+ name = nv[ 0 ].strip()
+ value = nv[ 1 ].strip()
+ if name in self.transformable_styles:
+ value = list(re.search(r'(\d+\.?\d*)(\D+)?', value).groups())
+ self.style[name] = float(value[0])
+ if value[1] and value[1] not in unit_convert:
+ logger.warning("Style '{}' has an unexpected unit: {}".format(style, value[1]))
+ else:
+ self.style[name] = value
+
+ # Parse transform attribute to update self.matrix
+ self.get_transformations(elt)
+
+ if self.style.get("display") == "none":
+ self.hidden = True
+
+ @staticmethod
+ def parse_name( tag ):
+ '''Read and return name from xml data'''
+ m = re.match( r'({(.+)})?(.+)', tag )
+ return {
+ 'namespace' : m.group( 2 ),
+ 'name' : m.group( 3 ),
+ }
+
+ def bbox(self):
+ '''Bounding box of all points'''
+ b_boxes = [x.bbox() for x in self.items]
+ if len( b_boxes ) < 1:
+ return (Point(0, 0), Point(0, 0))
+ xmin = min([b[0].x for b in b_boxes])
+ xmax = max([b[1].x for b in b_boxes])
+ ymin = min([b[0].y for b in b_boxes])
+ ymax = max([b[1].y for b in b_boxes])
+
+ return (Point(xmin,ymin), Point(xmax,ymax))
+
+ # Parse transform field
+ def get_transformations(self, elt):
+ '''Take an xml element and parse transformation commands
+ then apply the matrix and set any needed variables
+ '''
+ t = elt.get('transform')
+ if t is None: return
+
+ svg_transforms = [
+ 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY']
+
+ # match any SVG transformation with its parameter (until final parenthesis)
+ # [^)]* == anything but a closing parenthesis
+ # '|'.join == OR-list of SVG transformations
+ transforms = re.findall(
+ '|'.join([x + r'[^)]*\)' for x in svg_transforms]), t)
+
+ for t in transforms:
+ op, arg = t.split('(')
+ op = op.strip()
+ # Keep only numbers
+ arg = [float(x) for x in re.findall(number_re, arg)]
+ logger.debug('transform: ' + op + ' '+ str(arg))
+
+ if op == 'matrix':
+ self.matrix *= Matrix(arg)
+
+ if op == 'translate':
+ tx = arg[0]
+ if len(arg) == 1: ty = 0
+ else: ty = arg[1]
+ self.matrix *= Matrix([1, 0, 0, 1, tx, ty])
+
+ if op == 'scale':
+ sx = arg[0]
+ if len(arg) == 1: sy = sx
+ else: sy = arg[1]
+ self.xscale *= sx
+ self.yscale *= sy
+ self.matrix *= Matrix([sx, 0, 0, sy, 0, 0])
+
+ if op == 'rotate':
+ self.rotation += arg[0]
+ cos_a = math.cos(math.radians(arg[0]))
+ sin_a = math.sin(math.radians(arg[0]))
+ if len(arg) != 1:
+ tx, ty = arg[1:3]
+ self.matrix *= Matrix([1, 0, 0, 1, tx, ty])
+ self.matrix *= Matrix([cos_a, sin_a, -sin_a, cos_a, 0, 0])
+ if len(arg) != 1:
+ self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty])
+
+ if op == 'skewX':
+ tana = math.tan(math.radians(arg[0]))
+ self.matrix *= Matrix([1, 0, tana, 1, 0, 0])
+
+ if op == 'skewY':
+ tana = math.tan(math.radians(arg[0]))
+ self.matrix *= Matrix([1, tana, 0, 1, 0, 0])
+
+ def transform_styles(self, matrix):
+ '''Any style in this classes transformable_styles
+ will be scaled by the provided matrix.
+ If it has a unit type it will convert it to the proper value first.
+ '''
+ for style in self.transformable_styles:
+ if self.style.get(style):
+ has_units = re.search(r'\D+', self.style[style] if isinstance(self.style[style], str) else '')
+ xscale = abs(matrix.xscale())
+ yscale = abs(matrix.yscale())
+ if has_units is None:
+ self.style[style] = float(self.style[style]) * ((xscale+yscale)/2)
+ else:
+ unit = has_units.group().lower()
+ self.style[style] = float(re.search(r'\d+', self.style[style]).group()) * unit_convert.get(unit, 1) * ((xscale+yscale)/2)
+
+
+ def transform(self, matrix=None):
+ '''Apply the provided matrix. Default (None)
+ If no matrix is supplied then recursively apply
+ it's already existing matrix to all items.
+ '''
+ if matrix is None:
+ matrix = self.matrix
+ else:
+ matrix *= self.matrix
+ self.transform_styles(matrix)
+ for x in self.items:
+ x.transform(matrix)
+
+ def length(self, v, mode='xy'):
+ '''Return generic 2 dimensional length of svg element'''
+ # Handle empty (non-existing) length element
+ if v is None:
+ return 0
+
+ # Get length value
+ m = re.search(number_re, v)
+ if m: value = m.group(0)
+ else: raise TypeError(v + 'is not a valid length')
+
+ # Get length unit
+ m = re.search(unit_re, v)
+ if m: unit = m.group(0)
+ else: unit = None
+
+ if unit == '%':
+ if mode == 'x':
+ return float(value) * unit_convert[unit] * self.viewport.x
+ if mode == 'y':
+ return float(value) * unit_convert[unit] * self.viewport.y
+ if mode == 'xy':
+ return float(value) * unit_convert[unit] * self.viewport.x # FIXME
+
+ return float(value) * unit_convert[unit]
+
+ def xlength(self, x):
+ '''Length of element's x component'''
+ return self.length(x, 'x')
+ def ylength(self, y):
+ '''Length of element's y component'''
+ return self.length(y, 'y')
+
+ def flatten(self):
+ '''Flatten the SVG objects nested list into a flat (1-D) list,
+ removing Groups'''
+ # http://rightfootin.blogspot.fr/2006/09/more-on-python-flatten.html
+ # Assigning a slice a[i:i+1] with a list actually replaces the a[i]
+ # element with the content of the assigned list
+ i = 0
+ flat = copy.deepcopy(self.items)
+ while i < len(flat):
+ while isinstance(flat[i], Group):
+ flat[i:i+1] = flat[i].items
+ i += 1
+ return flat
+
+class Svg(Transformable):
+ '''SVG class: use parse to parse a file'''
+ # class Svg handles the