diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c960330 --- /dev/null +++ b/__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/elements.py b/elements.py new file mode 100644 index 0000000..fafa6aa --- /dev/null +++ b/elements.py @@ -0,0 +1,116 @@ +from enum import Enum + +class Transformation(object): + def __init__(self): + self.mirror_x = 0 + + self.absolute_rotation = 0 + self.absolute_magnification = 0 + + self.zoom = 1 + self.rotation = 0 + +class Element(object): + def __init__(self): + self.elflags = 0 + self.plex = 0 + +class Drawable(object): + def __init__(self): + self.layer = 0 + self.datatype = 0 + +class Boundary(Element, Drawable): + def __init__(self): + self.points = [] + +class Path(Element, Drawable): + class Styles(Enum): + SQUARE_ENDS = 0 + ROUNDED_ENDS = 1 + OFFSET_ENDS = 2 + CUSTOM_END = 4 + + def __init__(self): + self.extendEnd = [0,0] # extend past start and end + self.width = 0 + self.pathStyle = Path.Styles.SQUARE_ENDS + + self.points = [] + +class Text(Element, Drawable): + class VJust(Enum): + Top = 0 + Middle = 1 + Bottom = 2 + + class HJust(Enum): + Left = 0 + Center = 1 + Right = 2 + + def __init__(self): + # text info + self.string = "" + self.position = (0,0) + + # presentation + self.fontnumber = 0 + self.verticalJustification = Text.VJust.Top + self.horizontalJustification = Text.HJust.Left + + # optional path info + self.pathStype = Path.Styles.SQUARE_ENDS + self.pathWidth = 0 + + self.transformation = Transformation() + + +class Box(Element, Drawable): + def __init__(self): + self.points = [] + +class SRef(Element): + """ + A Structure Reference defines a single instance of a different structure in + the structure we call parent here + """ + def __init__(self): + self.position = (0,0) + self.structure = "" + + self.transformation = Transformation() + + self.parent = None + +class ARef(SRef): + """ + An Array Reference is similar to the SRef, but it defines the instances in a grid + with size instances spaces evenly between the bound coordinates + + with: + + v1 = bounds[0] - position + v2 = bounds[1] - position + i = xth instance index + j = yth instance index + + this will result in an array like this: + j\i | 0 | 1 | ... | size[0] + ----+-----------------+---------------------------+-----+-------------------- + 0 | (position) | (position + v1) | ... | (bounds[0]) + 1 | (position + v2) | (position + v1*i + v2*j) | ... | (bounds[0] + v2) + ... | ... | ... | ... | ... + size[1] | (bounds[2]) | (bounds[2] + v1) | ... | (bounds[0] + bounds[1]) + + """ + def __init__(self): + super(ARef, self).__init__() + + # positions of last instance in X and Y direction + self.bounds = [(0,0), (0,0)] + # number of instances in X and Y direction + self.size = (0,0) + + + diff --git a/library.py b/library.py new file mode 100644 index 0000000..4c11c9c --- /dev/null +++ b/library.py @@ -0,0 +1,81 @@ +from datetime import datetime +from .elements import SRef +from .reader import ProgressGetter + +class Library(object): + def __init__(self): + self.version = 0 + self.name = "NONAME" + + self.last_access = datetime.now() + self.last_mod = datetime.now() + + # unit setup + self.units_per_dbunit = 1 + self.meters_per_unit = 1 + + self.structures = {} + + class LinkError(Exception): + element = None + pass + + + """ + normalizes given coordinate according to library units + """ + def normalize_coord(self, coord, to_meters=False): + fact = self.units_per_dbunit + if to_meters: + fact *= self.meters_per_unit + + return (coord[0] * fact, coord[1] * fact) + + def link_refs(self, root, progress_callback=None): + for ref in root.references: + if isinstance(ref.structure, str): + try: + element.structure = self.structures[ref.structure] + except KeyError: + err = LinkError("dangeling sref (structure {} is not defined in library)".format(ref.structure)) + err.element = root + raise err + + return root + + def link_all_refs(self, progress_callback=None): + class Progress(ProgressGetter): + total = 0 + current = 0 + + def __init__(self, lib): + for key, value in lib.structures.items(): + self.total += len(value.references) + + def progress(self): + return float(self.current) / self.total + + def inc(self): + self.current += 1 + + count = Progress(self) + + for key, value in self.structures.items(): + for element in value.references: + if isinstance(element.structure, str): + # try to resolve link + try: + ref = self.structures[element.structure] + element.structure = ref + except KeyError: + err = LinkError("dangeling sref (structure {} is not defined in library)".format(element.structure)) + err.element = element + + raise err + + count.inc() + + if progress_callback: + progress_callback(count) + + \ No newline at end of file diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..44abf53 --- /dev/null +++ b/parser.py @@ -0,0 +1,407 @@ +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" + EXPECTED_SNAME = "expected SNAME" + EXPECTED_COLROW = "expected COLROW" + AREF_MISSING_POINTS = "aref needs three points" + INVALID_PATHTYPE = "pathtype must be in range [0,2]" + +class Warnings(Enum): + EXPECTED_ENDLIB = "missing end of library" + +class ParserError(Exception): + pass + +class Parser(Reader): + def __init__(self, file, progress_callback=None): + super(Parser, self).__init__(file) + + self._token = None + self.progress_callback = progress_callback + + # parser state + self.Library = Library() + self.current_structure = None + + @property + def token(self): + return self._token + + @token.setter + def token(self, t): + self._token = t + + def next_token(self, throw=False): + self.token = self.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.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.read_date() + self.Library.last_access = self.read_date() + + if not self.next_token() or self.token.ident != Records.LIBNAME: + raise ParserError(errors.EXPECTED_LIBNAME) + + self.Library.name = self.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.skip(self.token.len) + + elif self.token.ident == Records.FORMAT: + self.skip(self.token.len) + + # look for optional mask + while self.next_token().ident == Records.MASK: + self.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.read_double() + self.Library.meters_per_unit = self.read_double() + + while self.next_token() and self.parse_structure(): + pass + + if self.token.ident != Records.ENDLIB: + print(Warnings.EXPECTED_ENDLIB.value) + + # tell callback that the process completed + if self.progress_callback: + self.progress_callback(self) + + def parse_structure(self): + self.structure = Structure() + + if self.token.ident != Records.BGNSTR: + return False + + self.structure.last_mod = self.read_date() + self.structure.last_access = self.read_date() + + # read sname + if not self.next_token() or self.token.ident != Records.STRNAME: + raise ParserError(errors.EXPECTED_STRNAME) + + self.structure.name = self.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() + elif self.token.ident == Records.AREF: + self.parse_aref() + else: + self.skip(self.token.len) + + if self.progress_callback: + self.progress_callback(self) + + 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.read_short() + self.next_token(True) + + if self.token.ident == Records.PLEX: + element.plex = self.read_int() + self.next_token(True) + + def parse_layer(self, element): + if self.token.ident != Records.LAYER: + raise ParserError(errors.EXPECTED_LAYER) + + element.layer = self.read_ushort() + + def parse_boundary(self): + element = Boundary() + self.next_token(True) + self.parse_element(element) + self.parse_layer(element) + + if not self.next_token() or self.token.ident != Records.DATATYPE: + raise ParserError(errors.EXPECTED_DATATYPE) + + element.datatype = self.read_short() + + if not self.next_token() or self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINTS) + + element.points = self.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() + self.next_token(True) + self.parse_element(element) + self.parse_layer(element) + + if not self.next_token() or self.token.ident != Records.DATATYPE: + raise ParserError(errors.EXPECTED_DATATYPE) + + element.datatype = self.read_short() + + self.next_token(True) + + if self.token.ident == Records.PATHTYPE: + pathtype = self.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.read_int() + self.next_token(True) + + if self.token.ident == Records.BGNEXTN: + element.extendEnd[0] = self.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.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.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() + self.next_token(True) + self.parse_element(element) + self.parse_layer(element) + + if not self.next_token() or self.token.ident != Records.TEXTTYPE: + raise ParserError(errors.EXPECTED_TEXTTYPE) + + element.datatype = self.read_short() + + self.next_token(True) + + if self.token.ident == Records.PRESENTATION: + temp = self.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.read_short()) + self.next_token(True) + + if self.token.ident == Records.WIDTH: + element.pathWidth = self.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.read_coord() + + # skip potential array (if given) + self.skip(self.token.len - 8) + + if not self.next_token() or self.token.ident != Records.STRING: + raise ParserError(errors.EXPECTED_STRING) + + element.string = self.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.read_ushort() + 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.read_double() + self.next_token(True) + + if self.token.ident == Records.ANGLE: + trans.rotation = self.read_double() + self.next_token(True) + + def parse_box(self): + element = Box() + self.next_token(True) + self.parse_element(element) + self.parse_layer(element) + + if not self.next_token() or self.token.ident != Records.BOXTYPE: + raise ParserError(errors.EXPECTED_BOXTYPE) + + element.datatype = self.read_short() + + if not self.next_token() or self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINTS) + + element.points = self.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): + element = SRef() + element.parent = self.structure + + if not self.next_token(): + raise ParserError(errors.EXPECTED_SNAME) + + self.parse_element(element) + + if self.token.ident != Records.SNAME: + raise ParserError(errors.EXPECTED_SNAME) + + element.structure = self.read_ascii(self.token.len) + + self.next_token(True) + self.parse_strans(element.transformation) + + if self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINT) + + element.position = self.read_coord() + self.skip(self.token.len - 8) + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.references.append(element) + + def parse_aref(self): + element = ARef() + element.parent = self.structure + + if not self.next_token(): + raise ParserError(errors.EXPECTED_SNAME) + + self.parse_element(element) + + if self.token.ident != Records.SNAME: + raise ParserError(errors.EXPECTED_SNAME) + + element.structure = self.read_ascii(self.token.len) + + self.next_token(True) + + self.parse_strans(element.transformation) + + if self.token.ident != Records.COLROW: + raise ParserError(errors.EXPECTED_COLROW) + + element.size = (self.read_short(), self.read_short()) + + if not self.next_token() or self.token.ident != Records.XY: + raise ParserError(errors.EXPECTED_POINT) + + if self.token.len != 3*8: + raise ParserError(errors.AREF_MISSING_POINTS) + + element.position = self.read_coord() + element.bounds[0] = self.read_coord() + element.bounds[1] = self.read_coord() + + if not self.next_token() or self.token.ident != Records.ENDEL: + raise ParserError(errors.EXPECTED_ENDEL) + + self.structure.references.append(element) + +def parse_file(file, progress_func=None): + parser = Parser(file, progress_callback=progress_func) + parser.parse_lib() + + return parser.Library + + diff --git a/reader.py b/reader.py new file mode 100644 index 0000000..4c37e98 --- /dev/null +++ b/reader.py @@ -0,0 +1,148 @@ +from datetime import datetime +from .record import * +import abc +import ctypes + +class ProgressGetter(object, metaclass=abc.ABCMeta): + @abc.abstractproperty + def current(self): + pass + @abc.abstractproperty + def total(self): + pass + + @abc.abstractmethod + def progress(self): + raise NotImplementedError("getting the progess is not implemented") + +class Reader(ProgressGetter): + @property + def current(self): + return float(self.stream.tell()) + + # returns the read progress in percent + def progress(self): + if self.total <= 0: + return 1.0 + + return self.current / self.total + + @property + def total(self): + return self._total + + def __init__(self, file): + self.stream = file + + # find file size + self.stream.seek(0, 2) + self._total = self.stream.tell() + self.stream.seek(0, 0) + + def skip(self, n): + self.stream.read(n) + + def read_uint(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_ushort(self): + temp = self.stream.read(2) + if len(temp) != 2: + return None + + return int(temp[1]) | int(temp[0]) << 8 + + def read_short(self): + temp = self.read_ushort() + + if temp == None: + return None + + return ctypes.c_short(temp).value + + def read_int(self): + temp = self.read_uint() + + if temp == None: + return None + + return ctypes.c_int(temp).value + + 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 + for j in range(1,8): + result += float(temp[j])/(2.0**(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): + # removes zero terminators + # as well as trailing and beginning whitespaces + return self.stream.read(len).decode("ASCII").replace("\x00", "").strip() + + def read_record(self): + result = Record() + try: + result.len = self.read_ushort() + result.ident = Records(self.read_ushort()) + except ValueError: + return None + + result.len -= 4 # remove record header len + + return result + + def read_date(self): + # date + year = self.read_ushort() + month = self.read_ushort() + day = self.read_ushort() + + # time + hour = self.read_ushort() + minute = self.read_ushort() + second = self.read_ushort() + + 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/record.py b/record.py new file mode 100644 index 0000000..9c8ff63 --- /dev/null +++ b/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/structure.py b/structure.py new file mode 100644 index 0000000..50f9b70 --- /dev/null +++ b/structure.py @@ -0,0 +1,14 @@ +from datetime import datetime + +class Structure(object): + def __init__(self): + # metainfo + self.creation_date = datetime.now() + self.last_mod = datetime.now() + self.name = "NONAME" + + # contains all the low level elements + self.elements = [] + + # contains all sref and aref elements + self.references = [] \ No newline at end of file