From 5d939157ed6ca680568f267857156bdb5f1f55f3 Mon Sep 17 00:00:00 2001 From: Julian Daube Date: Mon, 1 Jul 2019 11:45:02 +0200 Subject: [PATCH] initial commit (work in progress) python version of parser works, BUT does not support AREF and SREF at the moment --- gds/.gitignore | 1 + gds/__init__.py | 7 + gds/elements.py | 76 ++++++++++ gds/library.py | 14 ++ gds/parser.py | 352 +++++++++++++++++++++++++++++++++++++++++++++++ gds/reader.py | 97 +++++++++++++ gds/record.py | 52 +++++++ gds/structure.py | 10 ++ test.py | 20 +++ 9 files changed, 629 insertions(+) create mode 100644 gds/.gitignore create mode 100644 gds/__init__.py create mode 100644 gds/elements.py create mode 100644 gds/library.py create mode 100644 gds/parser.py create mode 100644 gds/reader.py create mode 100644 gds/record.py create mode 100644 gds/structure.py create mode 100644 test.py diff --git a/gds/.gitignore b/gds/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/gds/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/gds/__init__.py b/gds/__init__.py new file mode 100644 index 0000000..c960330 --- /dev/null +++ b/gds/__init__.py @@ -0,0 +1,7 @@ +from .library import Library +from .structure import Structure +from .elements import * + +from .record import * +from .reader import Reader +from .parser import * diff --git a/gds/elements.py b/gds/elements.py new file mode 100644 index 0000000..9665b7a --- /dev/null +++ b/gds/elements.py @@ -0,0 +1,76 @@ +from enum import Enum + +class Transformation(object): + mirror_x = 0 + + absolute_rotation = 0 + absolute_magnification = 0 + + zoom = 1 + rotation = 0 + +class Element(object): + elflags = 0 + plex = 0 + datatype = 0 + +class Drawable(object): + layer = 0 + +class Boundary(Element, Drawable): + points = [] + +class Path(Element, Drawable): + class Styles(Enum): + SQUARE_ENDS = 0 + ROUNDED_ENDS = 1 + OFFSET_ENDS = 2 + CUSTOM_END = 4 + + extendEnd = [0,0] # extend past start and end + width = 0 + pathStyle = Styles.SQUARE_ENDS + + points = [] + +class Text(Element, Drawable): + class VJust(Enum): + Top = 0 + Middle = 1 + Bottom = 2 + + class HJust(Enum): + Left = 0 + Center = 1 + Right = 2 + + # text info + string = "" + position = (0,0) + + # presentation + fontnumber = 0 + verticalJustification = VJust.Top + horizontalJustification = HJust.Left + + # optional path info + pathStype = Path.Styles.SQUARE_ENDS + pathWidth = 0 + + transformation = Transformation() + + +class Box(Element, Drawable): + points = [] + +class SRef(Element): + Position = None + Structure = "" + + transformation = Transformation() + + # tree + Parent = None + Children = [] + + diff --git a/gds/library.py b/gds/library.py new file mode 100644 index 0000000..9389faf --- /dev/null +++ b/gds/library.py @@ -0,0 +1,14 @@ +from datetime import datetime + +class Library(object): + version = 0 + name = "NONAME" + + last_access = datetime.now() + last_mod = datetime.now() + + # unit setup + units_per_dbunit = 1 + meters_per_unit = 1 + + structures = {} \ No newline at end of file diff --git a/gds/parser.py b/gds/parser.py new file mode 100644 index 0000000..8b3362e --- /dev/null +++ b/gds/parser.py @@ -0,0 +1,352 @@ +from . import * +from enum import Enum + +class errors(Enum): + UNEXPECTED_EOF = "unexpected EOF" + EXPECTED_HEADER = "expected file header" + EXPECTED_BGNLIB = "expected beginning of library" + EXPECTED_LIBNAME = "Library name is missing" + EXPECTED_ENDMASK = "MASK records was not terminated by ENDMASK" + EXPECTED_STRNAME = "encountered nameless structure" + EXPECTED_UNITS = "no unit record in file" + EXPECTED_LAYER = "expected a layer record in element" + EXPECTED_POINTS = "expected coordinates for element" + EXPECTED_POINT = "expected coordinate for element" + EXPECTED_DATATYPE = "expected datatype" + EXPECTED_ENDEL = "end of element is missing" + EXPECTED_TEXTTYPE = "expected TEXTTYPE" + EXPECTED_STRING = "textelement is missing text" + EXPECTED_BOXTYPE = "expected BOXTYPE" + INVALID_PATHTYPE = "pathtype must be in range [0,2]" + +class Warnings(Enum): + EXPECTED_ENDLIB = "missing end of library" + +class ParserError(Exception): + pass + +class Parser(object): + # parser stack + _token = None + last_token = None + + # parser state + Library = Library() + current_structure = None + + def __init__(self, reader): + self.reader = reader + + @property + def token(self): + return self._token + + @token.setter + def token(self, t): + self.last_token = self._token + self._token = t + + def next_token(self, throw=False): + self.token = self.reader.read_record() + + if not self.token and throw: + raise ParserError(errors.UNEXPECTED_EOF) + + # if self.token: + # print(self.token.ident.name) + + return self.token + + def parse_lib(self): + # read header + if not self.next_token() or self.token.ident != Records.HEADER: + raise ParserError(errors.EXPECTED_HEADER) + + self.Library.version = self.reader.read_short() + + # must see BGNLIB + if not self.next_token() or self.token.ident != Records.BGNLIB: + raise ParserError(errors.EXPECTED_BGNLIB) + + self.Library.last_mod = self.reader.read_date() + self.Library.last_access = self.reader.read_date() + + if not self.next_token() or self.token.ident != Records.LIBNAME: + raise ParserError(errors.EXPECTED_LIBNAME) + + self.Library.name = self.reader.read_ascii(self.token.len) + + # read optional records + while self.next_token(): + if self.token.ident == Records.REFLIBS or \ + self.token.ident == Records.FONTS or \ + self.token.ident == Records.ATTRTABLE or \ + self.token.ident == Records.GENERATIONS: + # skip these records + self.reader.skip(self.token.len) + + elif self.token.ident == Records.FORMAT: + self.reader.skip(self.token.len) + + # look for optional mask + while self.next_token().ident == Records.MASK: + self.reader.skip(self.token.len) + + if self.token.ident != Records.ENDMASK: + raise ParserError(errors.EXPECTED_ENDMASK) + + else: + # continue + break + + if self.token.ident != Records.UNITS: + raise ParserError(errors.EXPECTED_UNITS) + + # read units + self.Library.units_per_dbunit = self.reader.read_double() + self.Library.meters_per_unit = self.reader.read_double() + + while self.next_token() and self.parse_structure(): + pass + + if self.token.ident != Records.ENDLIB: + print(Warnings.EXPECTED_ENDLIB.value) + + def parse_structure(self): + self.structure = Structure() + + if self.token.ident != Records.BGNSTR: + return False + + self.structure.last_mod = self.reader.read_date() + self.structure.last_access = self.reader.read_date() + + # read sname + if not self.next_token() or self.token.ident != Records.STRNAME: + raise ParserError(errors.EXPECTED_STRNAME) + + self.structure.name = self.reader.read_ascii(self.token.len) + + while self.next_token() and self.token.ident != Records.ENDSTR: + if self.token.ident == Records.BOUNDARY: + self.parse_boundary() + elif self.token.ident == Records.PATH: + self.parse_path() + elif self.token.ident == Records.TEXT: + self.parse_text() + elif self.token.ident == Records.SREF: + self.parse_sref() + else: + self.reader.skip(self.token.len) + + self.Library.structures[self.structure.name] = self.structure + self.structure = None + + return True + + def parse_element(self, element): + if self.token.ident == Records.ELFLAGS: + element.elflags = self.reader.read_short() + self.next_token(True) + + if self.token.ident == Records.PLEX: + element.plex = self.read_int() + self.next_token(True) + + + def parse_boundary(self): + element = Boundary() + if not self.next_token(): + raise ParserError(errors.EXPECTED_LAYER) + + self.parse_element(element) + + if self.token.ident == Records.LAYER: + element.layer = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.DATATYPE: + raise ParserError(errors.EXPECTED_DATATYPE) + + element.datatype = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINTS) + + element.points = self.reader.read_coords(self.token.len) + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.elements.append(element) + + def parse_path(self): + element = Path() + if not self.next_token(): + raise ParserError(errors.EXPECTED_LAYER) + + self.parse_element(element) + + if self.token.ident != Records.LAYER: + raise ParserError(errors.EXPECTED_LAYER) + + element.layer = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.DATATYPE: + raise ParserError(errors.EXPECTED_DATATYPE) + + element.datatype = self.reader.read_short() + + self.next_token(True) + + if self.token.ident == Records.PATHTYPE: + pathtype = self.reader.read_short() + if pathtype < 0 or pathtype > 4 or pathtype == 3: + raise ParserError(errors.INVALID_PATHTYPE) + + element.pathStyle = Path.Styles(pathtype) + self.next_token(True) + + if self.token.ident == Records.WIDTH: + element.width = self.reader.read_int() + self.next_token(True) + + if self.token.ident == Records.BGNEXTN: + element.extendEnd[0] = self.reader.read_int() + self.next_token(True) + elif element.pathStyle == Path.Styles.OFFSET_ENDS: + element.extendEnd[0] = element.width/2 + + if self.token.ident == Records.ENDEXTN: + element.extendEnd[1] = self.reader.read_int() + self.next_token(True) + elif element.pathStyle == Path.Styles.OFFSET_ENDS: + element.extendEnd[1] = element.width/2 + + if self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINTS) + + element.points = self.reader.read_coords(self.token.len) + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.elements.append(element) + + def parse_text(self): + element = Text() + if not self.next_token(): + raise ParserError(errors.EXPECTED_LAYER) + + self.parse_element(element) + + if not self.token.ident == Records.LAYER: + raise ParserError(errors.EXPECTED_LAYER) + + element.layer = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.TEXTTYPE: + raise ParserError(errors.EXPECTED_TEXTTYPE) + + element.datatype = self.reader.read_short() + + self.next_token(True) + + if self.token.ident == Records.PRESENTATION: + temp = self.reader.read_short() + element.fontnumber = (temp>>10)&0x03 + element.verticalJustification = Text.VJust((temp>>12)&0x03) + element.horizontalJustification = Text.HJust((temp>>14)&0X03) + + self.next_token(True) + + if self.token.ident == Records.PATHTYPE: + element.pathStype = Path.Styles(self.reader.read_short()) + self.next_token(True) + + if self.token.ident == Records.WIDTH: + element.pathWidth = self.reader.read_short() + self.next_token(True) + + self.parse_strans(element.transformation) + + if self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINT) + + element.point = self.reader.read_coord() + + # skip potential array (if given) + self.reader.skip(self.token.len - 8) + + if not self.next_token() or self.token.ident != Records.STRING: + raise ParserError(errors.EXPECTED_STRING) + + element.string = self.reader.read_ascii(self.token.len) + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.elements.append(element) + + def parse_strans(self, trans:Transformation): + if self.token.ident != Records.STRANS: + return + + flags = self.reader.read_short() + if flags & 0x01: + trans.mirror_x = True + + if flags & 0x2000: + trans.absolute_magnification = True + + if flags & 0x4000: + trans.absolute_rotation = True + + self.next_token(True) + + if self.token.ident == Records.MAG: + trans.zoom = self.reader.read_double() + self.next_token(True) + + if self.token.ident == Records.ANGLE: + trans.rotation = self.reader.read_double() + self.next_token(True) + + def parse_box(self): + element = Box() + if not self.next_token(): + raise ParserError(errors.EXPECTED_LAYER) + + self.parse_element(element) + + if self.token.ident != Records.LAYER: + raise ParserError(errors.EXPECTED_LAYER) + + element.layer = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.BOXTYPE: + raise ParserError(errors.EXPECTED_BOXTYPE) + + element.datatype = self.reader.read_short() + + if not self.next_token() or self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINTS) + + element.points = self.reader.read_coords(self.token.len) + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.elements.append(element) + + def parse_sref(self): + while self.next_token() and self.token.ident != Records.ENDEL: + self.reader.skip(self.token.len) + +def parse_file(file): + r = Reader(file) + parser = Parser(r) + parser.parse_lib() + + return parser.Library + + diff --git a/gds/reader.py b/gds/reader.py new file mode 100644 index 0000000..b24e617 --- /dev/null +++ b/gds/reader.py @@ -0,0 +1,97 @@ +from datetime import datetime +from .record import * + +class Reader(object): + def __init__(self, file): + self.stream = file + + def skip(self, n): + self.stream.read(n) + + def read_int(self): + temp = self.stream.read(4) + if len(temp) != 4: + return None + + return int(temp[3]) | int(temp[2]) << 8 | int(temp[1]) << 16 | int(temp[0]) << 24 + + def read_short(self): + temp = self.stream.read(2) + if len(temp) != 2: + return None + + return int(temp[1]) | int(temp[0]) << 8 + + def read_double(self): + temp = self.stream.read(8) + if len(temp) != 8: + return None + + result = 0 + + for i in temp: + if int(i) != 0: + # read double + result = 1 + + for j in range(1,8): + result += float(temp[8-j])/(2**(j*8)) + + exp = int(temp[0]) & 0x7F + exp -= 64 + result *= 16**exp + + if int(temp[0]) & 0x80: + result += -1 + + return result + + # double is Zero + return 0 + + def read_ascii(self, len): + return self.stream.read(len).decode("ASCII").strip() + + def read_record(self): + result = Record() + try: + result.len = self.read_short() + result.ident = Records(self.read_short()) + except ValueError: + return None + + result.len -= 4 # remove record header len + + return result + + def read_date(self): + # date + year = self.read_short() + month = self.read_short() + day = self.read_short() + + # time + hour = self.read_short() + minute = self.read_short() + second = self.read_short() + + return datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second) + + def read_coord(self): + X = self.read_int() + Y = self.read_int() + + return (X,Y) + + def read_coords(self, len): + len /= 8 + result = [] + while len > 0: + point = self.read_coord() + if not point: + return None + + result.append(point) + len -= 1 + + return result \ No newline at end of file diff --git a/gds/record.py b/gds/record.py new file mode 100644 index 0000000..9c8ff63 --- /dev/null +++ b/gds/record.py @@ -0,0 +1,52 @@ +from enum import Enum + +class Records(Enum): + UNKNOWN = 0x0000 + HEADER = 0x0002 + BGNLIB = 0x0102 + LIBNAME = 0x0206 + REFLIBS = 0x1F06 + FONTS = 0x2006 + ATTRTABLE = 0x2306 + GENERATIONS = 0x2202 + FORMAT = 0x3602 + MASK = 0x3706 + ENDMASKS = 0x3800 + UNITS = 0x0305 + ENDLIB = 0x0400 + BGNSTR = 0x0502 + STRNAME = 0x0606 + ENDEL = 0x1100 + ENDSTR = 0x0700 + BOUNDARY = 0x0800 + PATH = 0x0900 + SREF = 0x0A00 + AREF = 0x0B00 + TEXT = 0x0C00 + NODE = 0x1500 + BOX = 0x2D00 + ELFLAGS = 0x2601 + PLEX = 0x2F03 + LAYER = 0x0D02 + DATATYPE = 0x0E02 + XY = 0x1003 + PATHTYPE = 0x2102 + WIDTH = 0x0F03 + BGNEXTN = 0x3003 + ENDEXTN = 0x3103 + SNAME = 0x1206 + STRANS = 0x1A01 + MAG = 0x1B05 + ANGLE = 0x1C05 + COLROW = 0x1302 + TEXTTYPE = 0x1602 + PRESENTATION = 0x1701 + NODETYPE = 0x2A02 + BOXTYPE = 0x2E02 + STRING = 0x1906 + PROPATTR = 0x2B02 + PROPVALUE = 0x2C06 + +class Record(object): + ident = Records.UNKNOWN + len = 0 diff --git a/gds/structure.py b/gds/structure.py new file mode 100644 index 0000000..a37a653 --- /dev/null +++ b/gds/structure.py @@ -0,0 +1,10 @@ +from datetime import datetime + +class Structure(object): + # metainfo + creation_date = datetime.now() + last_mod = datetime.now() + name = "NONAME" + + # contains all the elements + elements = [] diff --git a/test.py b/test.py new file mode 100644 index 0000000..f702ba2 --- /dev/null +++ b/test.py @@ -0,0 +1,20 @@ +import gds +import sys + +for arg in sys.argv[1:]: + f = open(arg, "rb") + try: + lib = gds.parse_file(f) + print("file version: {}".format(lib.version)) + print("last access: {}".format(lib.last_access.isoformat())) + print("last modification: {}".format(lib.last_mod.isoformat())) + print("m/unit : {}".format(lib.meters_per_unit)) + print("unit/dbunit : {}".format(lib.units_per_dbunit)) + + print("library name : {}".format(lib.name)) + print("contains a total of {} structure(s)".format(len(lib.structures))) + + except gds.ParserError as e: + print("parser error: {}".format(e)) + finally: + f.close()