diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..9b4d540 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 +ARG VARIANT="3" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Option] Install Node.js +# ARG INSTALL_NODE="true" +# ARG NODE_VERSION="lts/*" +# RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +COPY requirements.txt /tmp/pip-tmp/ +RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ + && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends libxml2-dev libxslt-dev python-dev + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4354ede --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,48 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/python-3 +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 + "VARIANT": "3", + // Options + "INSTALL_NODE": "false", + "NODE_VERSION": "lts/*" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.gitignore b/.gitignore index 836277f..1d48c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -compiled files # -################## +# Compiled files # *.pyc venv @@ -7,4 +6,5 @@ venv *~ # Examples -/examples/* +/examples/ +.venv/ diff --git a/Javascript/main.html b/Javascript/main.html index 8577594..16bbb3f 100644 --- a/Javascript/main.html +++ b/Javascript/main.html @@ -3,7 +3,7 @@ Simplify an SVG - + @@ -39,7 +39,7 @@

Currently implemented

Note that not all transformations can be applied to every element. For example, scaling a circle differently in two dimensions will not result in a circle so has not been implemented. At present, only one transformation can be applied at a time. - You can apply it to multiple SVG elements as long as they are not nested (so no elements). + You can apply it to multiple SVG elements as long as they are not nested (so no <g> elements). Scaling an element will not effect its stroke width.

@@ -94,7 +94,7 @@

Matrix transformations

Run Tests
- + \ No newline at end of file diff --git a/Javascript/optimiser_tests.js b/Javascript/optimiser_tests.js index 964c3ba..d8117cb 100644 --- a/Javascript/optimiser_tests.js +++ b/Javascript/optimiser_tests.js @@ -222,5 +222,4 @@ var runTests = function() { testElementParsing(); testTransformationParsing(); testTransformations(); - } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..583ed16 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +Python program to clean up SVG files, particularly those created by Inkscape or Illustrator. + +# Use + +## Python +1. `python example.py` OR `python cleanSVG.py` + +## Javascript +1. `cd Javascript/` +1. `python3 -m http.server` +1. http://localhost:8000/ + +# Current Functionality + +## Remove attributes +- Remove attributes with a given name, e.g. remove 'id' attributes, which often aren't used. + +## Remove comments +- Removes all comments. + +## Remove elements +- Remove elements by their tag name. + +## Remove namespaces +- Remove all attributes associated with a given namespace, e.g. remove 'sodipodi' attributes created by Inkscape. + +## Remove redundant groups +- Move child elements outdside of group with no attributes, then delete group. + +## Set decimal places +- Rewrite attributes to a given number of decimal places. +- Strip out unnecessary trailing zeros. + +## Attributes +- `x`, `y`, `x1`, `y2`, `x2`, `y2` +- `cx`, `cy` +- `r`, `rx`, `ry` +- `width`, `height` +- `points` +- `d` + +## Apply transformations +- Applies transformations to elements so the attribute can be removed. + +* Translation + - In the form + - comma: (`12,34`) + - space(s): (`12 34`) + - comma and space(s): (`12, 34`), (`12 ,34`), (`12 , 34`) + - decimal: (`1.2, 3.4`) + - negative: (`-1.2, -3.4`) + + - Shapes + - `line` + - `rect` + - `circle`, `ellipse` + - `polyline`, `polygon` + - `path` (not fully tested) + - `g` (not fully tested) + +* Scale + - Shapes + - `path` + - `rect` + +## CSS stlying +- Convert individual style attributes to CSS styling. +- Remove default styles. + +# To Do + +## Remove namespaces +- [ ] Remove xml namespace if possible + +## Groups +- [ ] Remove unnecessary groups +- [ ] Remove unnecessary text groups +- [ ] Add groups in make styling and transforms more efficient + +## Transformations + +* Translation + - In the form + - single: (`12`) + + - Shapes + - `text` + - `tspan` + +* Rotation + - Shapes + - `path` + - `polyline/polygon` + - `line` + - `circle` + - `rect` -> `polygon`/`path`? + +* Scale + - Shapes + - `polyline`/`polygon` + - `line` + - `circle` -> `ellipse`? + +* SkewX and SkewY + - Shapes + - `line` + - `path` + - `polyline`/`polygon` + - `rect` -> `polygon`/`path`? + - `circle` -> `path arc`? + +* Matrix + - Shapes + - `line` + - `path` + - `polyline`/`polygon` + - `rect` -> `polygon`/`path`? + +## CSS styling +- [ ] Need to check whether style element already exists and whether class names already exist. +- [ ] Ideally find most efficient way to class elements for styling. + +# License +- [CREATIVE COMMONS PUBLIC LICENSE](./LICENSE) \ No newline at end of file diff --git a/cleanSVG.py b/cleanSVG.py index 4d455cf..3a81bf1 100644 --- a/cleanSVG.py +++ b/cleanSVG.py @@ -138,7 +138,7 @@ def parseFile(self, filename): try: self.tree = etree.parse(filename) except IOError: - print "Unable to open file", filename + print("Unable to open file", filename) sys.exit(1) self.root = self.tree.getroot() @@ -146,16 +146,16 @@ def parseFile(self, filename): def analyse(self): """ Search for namespaces. Will do more later """ - print "Namespaces:" + print("Namespaces:") for ns, link in self.root.nsmap.iteritems(): - print " %s: %s" % (ns, link) + print(" %s: %s" % (ns, link)) def removeGroups(self): """ Remove groups with no attributes """ # Doesn't work for nested groups for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, (str,bytes)): continue element_type = element.tag.split('}')[1] @@ -163,12 +163,11 @@ def removeGroups(self): parent = element.getparent() if parent is not None: parent_postion = parent.index(element) - print - print parent + print(parent) # Move children outside of group for i, child in enumerate(element, parent_postion): - print i - print "move %s to %s" % (child, i) + print(i) + print("move %s to %s" % (child, i)) parent.insert(i, child) #del parent[i] @@ -189,8 +188,8 @@ def toString(self, pretty_print=False): self._addStyleElement() if self.removeWhitespace: - svg_string = etree.tostring(self.root) - svg_string = re.sub(r'\n\s*' , "", svg_string) + svg_string = etree.tostring(self.root, encoding=str) + svg_string = re.sub(r'\n\s*', "", svg_string) else: svg_string = etree.tostring(self.root, pretty_print=pretty_print) @@ -204,7 +203,7 @@ def _addStyleElement(self): self.root.insert(0, style_element) style_text = '\n' - for styles, style_class in sorted(self.styles.iteritems(), key=lambda (k,v): v): + for styles, style_class in sorted(iter(self.styles.items()), key=lambda k_v: k_v[1]): style_text += "\t.%s{\n" % style_class for (style_id, style_value) in styles: style_text += '\t\t%s:\t%s;\n' % (style_id, style_value) @@ -218,7 +217,7 @@ def setDecimalPlaces(self, decimal_places): self.num_format = "%%.%df" % decimal_places for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, (str,bytes)): continue tag = element.tag.split('}')[1] @@ -230,7 +229,7 @@ def setDecimalPlaces(self, decimal_places): point_list = " ".join((formatted_values[i] + "," + formatted_values[i+1] for i in range(0, len(formatted_values), 2))) element.set("points", point_list) except IndexError: - print "Could not parse points list" + print("Could not parse points list") pass elif tag == "path": @@ -239,7 +238,7 @@ def setDecimalPlaces(self, decimal_places): element.set("d", coord_list) #for coord in coords: # if re_path_coords.match(coord): - # print coord + # print(coord) else: for attribute in element.attrib.keys(): @@ -251,20 +250,20 @@ def removeAttribute(self, attribute, exception_list=None): if exception_list is None: exception_list = [] - if self._verbose: print '\nRemoving attribute: %s' % attribute + if self._verbose: print('\nRemoving attribute: %s' % attribute) for element in self.tree.iter(): if attribute in element.attrib.keys() and element.attrib[attribute] not in exception_list: - if self._verbose: print ' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute]) + if self._verbose: print(' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute])) del element.attrib[attribute] def removeElement(self, tagName): """ Remove all instances of an element. """ - if self._verbose: print '\nRemoving element: %s' % tagName + if self._verbose: print('\nRemoving element: %s' % tagName) for element in self.tree.iter(): - if (isinstance(element.tag, basestring)): + if isinstance(element.tag, (str,bytes)): tag = element.tag.split('}')[1] if tag == tagName: element.getparent().remove(element) @@ -272,7 +271,7 @@ def removeElement(self, tagName): def removeComments(self): """ Remove all comments. """ - if self._verbose: print '\nRemoving comments' + if self._verbose: print('\nRemoving comments') for element in self.tree.iter(): if element.tag is etree.Comment: @@ -284,7 +283,7 @@ def removeNonDefIDAttributes(self): def_IDs = [] for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, (str,bytes)): continue tag = element.tag.split('}')[1] @@ -302,9 +301,9 @@ def removeNamespace(self, namespace): nslink = self.root.nsmap.get(namespace) if self._verbose: - print "\nRemoving namespace, %s" % namespace + print("\nRemoving namespace, %s" % namespace) if nslink: - print " - Link: %s" % nslink + print(" - Link: %s" % nslink) if nslink: nslink = "{%s}" % nslink @@ -314,13 +313,13 @@ def removeNamespace(self, namespace): if element.tag[:length] == nslink: self.root.remove(element) if self._verbose: - print " - removed element: %s" % element.tag[length:] + print(" - removed element: %s" % element.tag[length:]) for attribute in element.attrib.keys(): if attribute[:length] == nslink: del element.attrib[attribute] if self._verbose: - print " - removed attribute from tag: %s" % element.tag + print(" - removed attribute from tag: %s" % element.tag) del self.root.nsmap[namespace] @@ -393,7 +392,7 @@ def applyTransforms(self): def _applyGroupTransforms(self, group_element, transformations): # Ensure all child elements are paths - children = [child for child in group_element if isinstance(child.tag, basestring)] + children = [child for child in group_element if isinstance(child.tag, (str,bytes))] if any((child.tag.split('}')[1] != 'path' for child in children)): return @@ -431,7 +430,7 @@ def _formatNumber(self, number): return str_number def _translateElement(self, element, delta): - #print " - translate by: (%s, %s)" % delta + #print(" - translate by: (%s, %s)" % delta) element_type = element.tag.split('}')[1] coords = position_attributes.get(element_type) diff --git a/cleaned-test.svg b/cleaned-test.svg new file mode 100644 index 0000000..a12b6bf --- /dev/null +++ b/cleaned-test.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example.py b/example.py index 066c8d4..3933e28 100644 --- a/example.py +++ b/example.py @@ -1,16 +1,16 @@ from cleanSVG import CleanSVG import os -input_file = os.path.join("examples", "paths_test.svg") +input_file = os.path.join("examples", "paths.svg") output_file = "cleaned-test.svg" svg = CleanSVG(input_file) -svg.removeAttribute('id') +#svg.removeAttribute('id') svg.setDecimalPlaces(1) svg.extractStyles() -svg.removeElement('title') -svg.removeElement('desc') -svg.removeElement('defs') -svg.removeComments() +#svg.removeElement('title') +#svg.removeElement('desc') +#svg.removeElement('defs') +#svg.removeComments() svg.applyTransforms() svg.write(output_file) \ No newline at end of file diff --git a/readme.md b/readme.md deleted file mode 100644 index 1db9844..0000000 --- a/readme.md +++ /dev/null @@ -1,117 +0,0 @@ -Python program to clean up SVG files, particularly those created by Inkscape or Illustrator - - - -# --- Current Functionality --- - -## Remove attributes - Remove attributes with a given name, e.g. remove 'id' attributes, which often aren't used. - -## Remove comments - Removes all comments. - -## Remove elements - Remove elements by their tag name. - -## Remove namespaces - Remove all attributes associated with a given namespace, e.g. remove 'sodipodi' attributes created by Inkscape. - -## Remove redundant groups - Move child elements outdside of group with no attributes, then delete group. - -## Set decimal places - Rewrite attributes to a given number of decimal places. - Strip out unnecessary trailing zeros. - -* Attributes - - x, y, x1, y2, x2, y2 - - cx, cy - - r, rx, ry - - width, height - - points - - d - -## Apply transformations - Applies transformations to elements so the attribute can be removed. - -* Translation - - In the form - - comma: (12,34) - - space(s): (12 34) - - comma and space(s): (12, 34), (12 ,34), (12 , 34) - - decimal: (1.2, 3.4) - - negative: (-1.2, -3.4) - - - Shapes - - line - - rect - - circle, ellipse - - polyline, polygon - - path (not fully tested) - - g (not fully tested) - -* Scale - - Shapes - - path - - rect - -## CSS stlying - Convert individual style attributes to CSS styling. - Remove default styles. - -# --- To Do --- - -## Remove namespaces - Remove xml namespace if possible - -## Groups - Remove unnecessary groups - Remove unnecessary text groups - Add groups in make styling and transforms more efficient - -## Transformations - -* Translation - - In the form - - single: (12) - - - Shapes - - text - - tspan - -* Rotation - - Shapes - - path - - polyline/polygon - - line - - circle - - rect -> polygon/path? - -* Scale - - Shapes - - polyline/polygon - - line - - circle -> ellipse? - -* SkewX and SkewY - - Shapes - - line - - path - - polyline/polygon - - rect -> polygon/path? - - circle -> path arc? - -* Matrix - - Shapes - - line - - path - - polyline/polygon - - rect -> polygon/path? - -## CSS styling - Need to check whether style element already exists and whether class names already exist. - Ideally find most efficient way to class elements for styling. - -## License - - diff --git a/requirements.txt b/requirements.txt index ed3da4f..8971756 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml==3.2.0 +lxml==4.6.3