Added script for converting GDS to SVG
Signed-off-by: Grzegorz Latosinski <glatosinski@antmicro.com>
This commit is contained in:
parent
f1d096dac2
commit
7294a3e46e
|
@ -0,0 +1,201 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
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 "([^"]*)".')
|
||||||
|
|
||||||
|
debug = True
|
||||||
|
superdebug = True
|
||||||
|
|
||||||
|
|
||||||
|
def _magic_tcl_header(ofile, gdsfile):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def run_magic(destdir, tcl_path, input_techfile, d="null"):
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
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([
|
||||||
|
"--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)
|
||||||
|
|
||||||
|
|
||||||
|
def run_inkscape(args):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
'input_gds',
|
||||||
|
help="Path to the input .gds file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'input_tech',
|
||||||
|
help="Path to the input .tech file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-svg',
|
||||||
|
help='Path to the output .svg file'
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
convert_to_svg(args.input_gds, args.input_tech, args.output_svg)
|
Loading…
Reference in New Issue