diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py
index 9e60a99..c9483b8 100644
--- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/gds_to_svg.py
@@ -17,209 +17,143 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
+"""
+Module for generating cell layouts for given technology files and GDS files.
+
+Creates cell layout SVG files from GDS cell files using `magic` tool.
+"""
+
 import sys
 import os
 import re
-import subprocess
 import argparse
 
-FATAL_ERROR = re.compile('((Error parsing)|(No such file or directory)|(couldn\'t be read))')  # noqa: E501
-READING_REGEX = re.compile('Reading "([^"]*)".')
+sys.path.insert(0, os.path.abspath(__file__ + '../../../'))
 
-debug = True
-superdebug = True
+from skywater_pdk.tools import magic, draw  # noqa: E402
 
 
-def _magic_tcl_header(ofile, gdsfile):
+def convert_gds_to_svg(
+        input_gds,
+        input_techfile,
+        output_svg=None,
+        tmp_tcl=None,
+        keep_temporary_files=False) -> int:
     """
-    Adds a header to TCL file.
+    Converts GDS file to SVG cell layout diagram.
+
+    Generates TCL script for drawing a cell layout in `magic` tool and creates
+    a SVG file with the diagram.
 
     Parameters
     ----------
-    ofile: output file stream
-    gdsfile: path to GDS file
-    """
-    print('#!/bin/env wish',         file=ofile)
-    print('drc off',                 file=ofile)
-    print('scalegrid 1 2',           file=ofile)
-    print('cif istyle vendorimport', file=ofile)
-    print('gds readonly true',       file=ofile)
-    print('gds rescale false',       file=ofile)
-    print('tech unlock *',           file=ofile)
-    print('cif warning default',     file=ofile)
-    print('set VDD VPWR',            file=ofile)
-    print('set GND VGND',            file=ofile)
-    print('set SUB SUBS',            file=ofile)
-    print('gds read ' + gdsfile,     file=ofile)
+    input_gds : str
+        Path to input GDS file
+    input_techfile : str
+        Path to input technology definition file (.tech)
+    output_svg : str
+        Path to output SVG file
+    keep_temporary_files : bool
+        Determines if intermediate TCL script should be kept
 
-
-def run_magic(destdir, tcl_path, input_techfile, d="null"):
-    """
-    Runs magic to generate layout files.
-
-    Parameters
-    ----------
-    destdir: destination directory
-    tcl_path: path to input TCL file
-    input_techfile: path to the technology file
-    d: Workstation type, can be NULL, X11, OGL or XWIND
-    """
-    cmd = [
-        'magic',
-        '-nowrapper',
-        '-d'+d,
-        '-noconsole',
-        '-T', input_techfile,
-        os.path.abspath(tcl_path)
-    ]
-    with open(tcl_path.replace(".tcl", ".sh"), "w") as f:
-        f.write("#!/bin/sh\n")
-        f.write(" ".join(cmd))
-    mproc = subprocess.run(
-        cmd,
-        stdin=subprocess.DEVNULL,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-        cwd=destdir,
-        universal_newlines=True)
-    assert mproc.stdout
-    max_cellname_width = 0
-    output_by_cells = [('', [])]
-    fatal_errors = []
-    for line in mproc.stdout.splitlines():
-        cifwarn = ('CIF file read warning: Input off lambda grid by 1/2; ' +
-                   'snapped to grid')
-        if line.startswith(cifwarn):
-            continue
-        m = FATAL_ERROR.match(line)
-        if m:
-            fatal_errors.append(line)
-        m = READING_REGEX.match(line)
-        if m:
-            cell_name = m.group(1)
-            max_cellname_width = max(max_cellname_width, len(cell_name))
-            output_by_cells.append((cell_name, []))
-        output_by_cells[-1][-1].append(line)
-    for cell, lines in output_by_cells:
-        prefix = "magic " + cell.ljust(max_cellname_width) + ':'
-        for line in lines:
-            is_error = 'rror' in line
-            if superdebug or (debug and is_error):
-                print(prefix, line)
-    assert not mproc.stderr, mproc.stderr
-    if mproc.returncode != 0 or fatal_errors:
-        if fatal_errors:
-            msg = ['ERROR: Magic had fatal errors in output:'] + fatal_errors
-        else:
-            msg = ['ERROR: Magic exited with status ' + str(mproc.returncode)]
-        msg.append("")
-        msg.append(" ".join(cmd))
-        msg.append('='*75)
-        msg.append(mproc.stdout)
-        msg.append('='*75)
-        msg.append(destdir)
-        msg.append(tcl_path)
-        msg.append('-'*75)
-        msg.append(msg[0])
-        raise SystemError('\n'.join(msg))
-    return output_by_cells
-
-
-def convert_to_svg(input_gds, input_techfile, output=None):
-    """
-    Converts GDS file to a SVG layout image.
-
-    Parameters
-    ----------
-    input_gds: path to input GDS file
-    input_techfile: path to the technology file
-    output: optional path to the final SVG file
+    Returns
+    -------
+    int : 0 if finished successfully, error code from `magic` otherwise
     """
     input_gds = os.path.abspath(input_gds)
-    input_techfile = os.path.abspath(input_techfile)
-    destdir, gdsfile = os.path.split(input_gds)
-    basename, ext = os.path.splitext(gdsfile)
-    if output:
-        output_svg = output
+    if output_svg:
+        output_svg = os.path.abspath(output_svg)
+        destdir, _ = os.path.split(output_svg)
     else:
-        output_svg = os.path.join(destdir, "{}.svg".format(basename))
-    assert not os.path.exists(output_svg), output_svg + " already exists!?"
-    tcl_path = os.path.join(destdir, "{}.gds2svg.tcl".format(basename))
-    with open(tcl_path, 'w') as ofile:
-        _magic_tcl_header(ofile, input_gds)
-        ofile.write("load " + basename + "\n")
-        ofile.write("box 0 0 0 0\n")
-        ofile.write("select top cell\n")
-        ofile.write("expand\n")
-        ofile.write("view\n")
-        ofile.write("select clear\n")
-        ofile.write("box position -1000 -1000\n")
-        ofile.write("plot svg " + basename + ".tmp1.svg\n")
-        ofile.write("quit -noprompt\n")
-    run_magic(destdir, tcl_path, input_techfile, " XR")
-    tmp1_svg = os.path.join(destdir, "{}.tmp1.svg".format(basename))
-    tmp2_svg = os.path.join(destdir, "{}.tmp2.svg".format(basename))
-    assert os.path.exists(tmp1_svg), tmp1_svg + " doesn't exist!?"
-    os.unlink(tcl_path)
-    for i in range(0, 3):
-        # Remove the background
-        with open(tmp1_svg) as f:
-            data = f.read()
-        data = re.sub(
-            '<rect[^>]* style="[^"]*fill-opacity:1;[^"]*"/>',
-            '',
-            data
-        )
-        with open(tmp2_svg, 'w') as f:
-            f.write(data)
-        # Use inkscape to crop
-        retcode = run_inkscape([
+        destdir, name = os.path.split(input_gds)
+        output_svg = os.path.join(destdir, f'{name}.svg')
+    input_techfile = os.path.abspath(input_techfile)
+
+    workdir, _ = os.path.split(input_techfile)
+
+    if output_svg:
+        filename, _ = os.path.splitext(output_svg)
+        if not tmp_tcl:
+            tmp_tcl = f'{filename}.tcl'
+    try:
+        tmp_tcl, output_svg = magic.create_tcl_plot_script_for_gds(
+            input_gds,
+            tmp_tcl,
+            output_svg)
+        magic.run_magic(
+            tmp_tcl,
+            input_techfile,
+            workdir,
+            display_workstation='XR')
+
+        if not keep_temporary_files:
+            if os.path.exists(tmp_tcl):
+                os.unlink(tmp_tcl)
+        assert os.path.exists(output_svg), f'Magic did not create {output_svg}'
+    except magic.MagicError as err:
+        if not keep_temporary_files:
+            if os.path.exists(tmp_tcl):
+                os.unlink(tmp_tcl)
+        print(err)
+        return err.errorcode
+    except Exception:
+        if not keep_temporary_files:
+            if os.path.exists(tmp_tcl):
+                os.unlink(tmp_tcl)
+        raise
+    return 0
+
+
+def cleanup_gds_diagram(input_svg, output_svg) -> int:
+    """
+    Crops and cleans up GDS diagram.
+
+    Parameters
+    ----------
+    input_svg : str
+        Input SVG file with cell layout
+    output_svg : str
+        Output SVG file with cleaned cell layout
+
+    Returns
+    -------
+    int : 0 if successful, error code from Inkscape otherwise
+    """
+    with open(input_svg, 'r') as f:
+        data = f.read()
+    data = re.sub(
+        '<rect[^>]* style="[^"]*fill-opacity:1;[^"]*"/>',
+        '',
+        data
+    )
+    with open(output_svg, 'w') as f:
+        f.write(data)
+    result = draw.run_inkscape([
             "--verb=FitCanvasToDrawing",
             "--verb=FileSave",
             "--verb=FileClose",
             "--verb=FileQuit",
-            tmp2_svg])
-        if retcode == 0:
-            break
-    for i in range(0, 3):
-        # Convert back to plain SVG
-        retcode = run_inkscape([
-            "--export-plain-svg=%s" % (tmp2_svg),
-            "--export-background-opacity=1.0",
-            tmp2_svg])
-        if retcode == 0:
-            break
-    os.unlink(tmp1_svg)
-    # Move into the correct location
-    os.rename(tmp2_svg, output_svg)
-    print("Created", output_svg)
+            output_svg
+        ],
+        3)
+    if result[-1] != 0:
+        return result[-1]
 
-
-def run_inkscape(args):
-    """
-    Run Inkscape with given arguments.
-
-    Parameters
-    ----------
-    args: List of arguments to be passed to Inkscape
-    """
-    p = subprocess.Popen(["inkscape"] + args)
-    try:
-        p.wait(timeout=60)
-    except subprocess.TimeoutExpired:
-        print("ERROR: Inkscape timed out! Sending SIGTERM")
-        p.terminate()
-        try:
-            p.wait(timeout=60)
-        except subprocess.TimeoutExpired:
-            print("ERROR: Inkscape timed out! Sending SIGKILL")
-            p.kill()
-        p.wait()
-    return p.returncode
+    result = draw.run_inkscape([
+            f'--export-plain-svg={output_svg}',
+            '--existsport-background-opacity=1.0',
+            output_svg
+        ],
+        3)
+    return result[-1]
 
 
 def main(argv):
-    parser = argparse.ArgumentParser(argv[0])
+    parser = argparse.ArgumentParser(
+        prog=argv[0],
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter
+    )
     parser.add_argument(
         'input_gds',
         help="Path to the input .gds file"
@@ -232,9 +166,42 @@ def main(argv):
         '--output-svg',
         help='Path to the output .svg file'
     )
+    parser.add_argument(
+        '--output-tcl',
+        help='Path to temporary TCL file'
+    )
+    parser.add_argument(
+        '--keep-temporary-files',
+        help='Keep the temporary files in the end',
+        action='store_true'
+    )
     args = parser.parse_args(argv[1:])
-    convert_to_svg(args.input_gds, args.input_tech, args.output_svg)
-    return 0
+
+    if args.output_svg:
+        filename, _ = os.path.splitext(args.output_svg)
+        tmp_svg = f'{filename}.tmp.svg'
+    else:
+        filename, _ = os.path.splitext(args.input_gds)
+        tmp_svg = f'{filename}.tmp.svg'
+        args.output_svg = f'{filename}.svg'
+
+    result = convert_gds_to_svg(
+        args.input_gds,
+        args.input_tech,
+        tmp_svg,
+        args.output_tcl,
+        args.keep_temporary_files
+    )
+
+    if result != 0:
+        return result
+
+    result = cleanup_gds_diagram(tmp_svg, args.output_svg)
+
+    if not args.keep_temporary_files:
+        os.unlink(tmp_svg)
+
+    return result
 
 
 if __name__ == '__main__':
diff --git a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py
index 2ab151e..dada432 100644
--- a/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/gds_to_svg/generate-gds-svgs.py
@@ -17,6 +17,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
+"""
+Creates cell layouts for cells with GDS files in the skywater-pdk libraries.
+"""
+
 import argparse
 from pathlib import Path
 import sys
@@ -24,11 +28,15 @@ import contextlib
 import traceback
 import errno
 
-from gds_to_svg import convert_to_svg
+from gds_to_svg import convert_gds_to_svg
 
 
 def main(argv):
-    parser = argparse.ArgumentParser(prog=argv[0])
+    parser = argparse.ArgumentParser(
+        prog=argv[0],
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter
+    )
     parser.add_argument(
         'libraries_dir',
         help='Path to the libraries directory of skywater-pdk',
@@ -92,7 +100,7 @@ def main(argv):
                         print('Run the script with --create-dirs')
                         return errno.ENOENT
                 outfile = outdir / gdsfile.with_suffix('.svg').name
-                convert_to_svg(gdsfile, args.tech_file, outfile)
+                convert_gds_to_svg(gdsfile, args.tech_file, outfile)
             except Exception:
                 print(
                     f'Failed to generate cell layout for {str(gdsfile)}',
diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py b/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py
new file mode 100644
index 0000000..646d462
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/tools/draw.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 SkyWater PDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+A module for altering diagrams and image files using Inkscape and other tools.
+"""
+
+import subprocess
+
+
+def run_inkscape(args, retries=1, inkscape_executable='inkscape') -> int:
+    """
+    Runs Inkscape for given arguments.
+
+    Parameters
+    ----------
+    args : List[str]
+        List of arguments to provide to Inkscape
+    retries : int
+        Number of tries to run Inkscape with given arguments
+
+    Returns
+    Union[List[int], int] : error codes for Inkscape runs
+    """
+    returncodes = []
+    for i in range(retries):
+        p = subprocess.Popen([inkscape_executable] + args)
+        try:
+            p.wait(timeout=60)
+        except subprocess.TimeoutExpired:
+            print("ERROR: Inkscape timed out! Sending SIGTERM")
+            p.terminate()
+            try:
+                p.wait(timeout=60)
+            except subprocess.TimeoutExpired:
+                print("ERROR: Inkscape timed out! Sending SIGKILL")
+                p.kill()
+            p.wait()
+        if retries == 1:
+            return p.returncode
+        returncodes.append(p.returncode)
+        if p.returncode == 0:
+            break
+    return returncodes
diff --git a/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py b/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py
new file mode 100644
index 0000000..4a42cd7
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/tools/magic.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 SkyWater PDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+A module containing various functions related to `magic` tool
+"""
+
+import os
+import re
+import subprocess
+from typing import Tuple
+
+FATAL_ERROR = re.compile('((Error parsing)|(No such file or directory)|(couldn\'t be read))')  # noqa: E501
+
+
+class MagicError(Exception):
+    """
+    Raised when there are errors in magic tool execution.
+    """
+    def __init__(self, message, errorcode):
+        self.errorcode = errorcode
+        super().__init__(message)
+
+
+def add_magic_tcl_header(ofile, gdsfile):
+    """
+    Adds a header to a TCL file.
+
+    Parameters
+    ----------
+    ofile : TextIOWrapper
+        output file stream
+    gdsfile : str
+        path to GDS file
+    """
+    ofile.write('#!/bin/env wish\n')
+    ofile.write('drc off\n')
+    ofile.write('scalegrid 1 2\n')
+    ofile.write('cif istyle vendorimport\n')
+    ofile.write('gds readonly true\n')
+    ofile.write('gds rescale false\n')
+    ofile.write('tech unlock *\n')
+    ofile.write('cif warning default\n')
+    ofile.write('set VDD VPWR\n')
+    ofile.write('set GND VGND\n')
+    ofile.write('set SUB SUBS\n')
+    ofile.write(f'gds read {gdsfile}\n')
+
+
+def create_tcl_plot_script_for_gds(
+        input_gds,
+        output_tcl=None,
+        output_svg=None) -> Tuple[str, str]:
+    """
+    Creates TCL script for creating cell layout image from GDS file.
+
+    Parameters
+    ----------
+    input_gds : str
+        Path to GDS file
+    output_tcl : str
+        Path to created TCL file
+    output_svg : str
+        Path where the SVG file created by output_tcl script should be located
+
+    Returns
+    -------
+    Tuple(str, str) : paths to TCL and SVG files (can be used if autogenerated)
+    """
+    input_gds = os.path.abspath(input_gds)
+
+    destdir, gdsfile = os.path.split(input_gds)
+    basename, ext = os.path.splitext(gdsfile)
+
+    if not output_tcl:
+        output_tcl = os.path.join(destdir, f'{basename}.gds2svg.tcl')
+
+    if not output_svg:
+        output_svg = os.path.join(destdir, f'{basename}.tmp.svg')
+
+    with open(output_tcl, 'w') as ofile:
+        add_magic_tcl_header(ofile, input_gds)
+        ofile.write(f"load {basename}\n")
+        ofile.write("box 0 0 0 0\n")
+        ofile.write("select top cell\n")
+        ofile.write("expand\n")
+        ofile.write("view\n")
+        ofile.write("select clear\n")
+        ofile.write("box position -1000 -1000\n")
+        ofile.write(f"plot svg {output_svg}\n")
+        ofile.write("quit -noprompt\n")
+
+    return output_tcl, output_svg
+
+
+def run_magic(
+        tcl_file,
+        technology_file,
+        workdir=None,
+        display_workstation='NULL',
+        debug=False,
+        magic_executable='magic') -> int:
+    """
+    Generates layout files for a given TCL file and technology file.
+
+    Uses `magic` tool for generating the layout.
+
+    Parameters
+    ----------
+    tcl_file : str
+        path to input TCL file
+    technology_file : str
+        path to the technology file
+    workdir : str
+        path to the working directory for `magic`
+    display_workstation : str
+        graphics interface, can be NULL, X11 or OpenGL
+    debug : bool
+        True if all output from `magic` tool should be displayed
+    magic_executable : str
+        Path to `magic` executable
+
+    Returns
+    -------
+        int: return code for `magic` tool
+
+    Raises
+    ------
+    AssertionError
+        Raised when display_workstation is not NULL, X11 or OpenGL
+    MagicError
+        Raised when `magic` tool failed to run for given files
+    """
+    assert display_workstation in ['NULL', 'X11', 'OpenGL', 'XR']
+    cmd = [
+        magic_executable,
+        '-nowrapper',
+        '-noconsole',
+        f'-d{display_workstation}',
+        f'-T{os.path.abspath(technology_file)}',
+        '-D' if debug else '',
+        os.path.abspath(tcl_file)
+    ]
+    result = subprocess.run(
+        cmd,
+        stdin=subprocess.DEVNULL,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        cwd=workdir,
+        universal_newlines=True
+    )
+
+    errors_present = False
+    for line in result.stdout.splitlines():
+        m = FATAL_ERROR.match(line)
+        if m:
+            errors_present = True
+            break
+
+    if result.returncode != 0 or errors_present:
+        msg = ['ERROR: There were fatal errors in magic.']
+        msg += result.stdout.splitlines()
+        msg += [f'ERROR: Magic exited with status {result.returncode}']
+        msg.append("")
+        msg.append(" ".join(cmd))
+        msg.append('='*75)
+        msg.append(result.stdout)
+        msg.append('='*75)
+        msg.append(tcl_file)
+        msg.append('-'*75)
+        msg.append(msg[0])
+        raise MagicError('\n'.join(msg), result.returncode)
+
+    if debug:
+        print(result.stdout)