You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
17 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.record` --- GDSII record I/O
========================================
This module contains classes for low-level GDSII I/O.
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
from __future__ import absolute_import
from . import exceptions, tags, types
from datetime import datetime
import math
import struct
__all__ = [
'Record',
'Reader'
]
_RECORD_HEADER_FMT = struct.Struct('>HH')
def _parse_nodata(data):
"""Parse :const:`NODATA` data type. Does nothing."""
def _parse_bitarray(data):
"""
Parse :const:`BITARRAY` data type.
>>> _parse_bitarray(b'ab') # ok, 2 bytes
24930
>>> _parse_bitarray(b'abcd') # too long
Traceback (most recent call last):
...
IncorrectDataSize: BITARRAY
>>> _parse_bitarray('') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: BITARRAY
"""
if len(data) != 2:
raise exceptions.IncorrectDataSize('BITARRAY')
(val,) = struct.unpack('>H', data)
return val
def _parse_int2(data):
"""
Parse INT2 data type.
>>> _parse_int2(b'abcd') # ok, even number of bytes
(24930, 25444)
>>> _parse_int2(b'abcde') # odd number of bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT2
>>> _parse_int2(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT2
"""
data_len = len(data)
if not data_len or (data_len % 2):
raise exceptions.IncorrectDataSize('INT2')
return struct.unpack('>%dh' % (data_len//2), data)
def _parse_int4(data):
"""
Parse INT4 data type.
>>> _parse_int4(b'abcd')
(1633837924,)
>>> _parse_int4(b'abcdef') # not divisible by 4
Traceback (most recent call last):
...
IncorrectDataSize: INT4
>>> _parse_int4(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT4
"""
data_len = len(data)
if not data_len or (data_len % 4):
raise exceptions.IncorrectDataSize('INT4')
return struct.unpack('>%dl' % (data_len//4), data)
def _int_to_real(num):
"""
Convert REAL8 from internal integer representation to Python reals.
Zeroes:
>>> print(_int_to_real(0x0))
0.0
>>> print(_int_to_real(0x8000000000000000)) # negative
0.0
>>> print(_int_to_real(0xff00000000000000)) # denormalized
0.0
Others:
>>> print(_int_to_real(0x4110000000000000))
1.0
>>> print(_int_to_real(0xC120000000000000))
-2.0
"""
sgn = -1 if 0x8000000000000000 & num else 1
mant = num & 0x00ffffffffffffff
exp = (num >> 56) & 0x7f
return math.ldexp(sgn * mant, 4 * (exp - 64) - 56)
def _parse_real8(data):
"""
Parse REAL8 data type.
>>> _parse_real8(struct.pack('>3Q', 0x0, 0x4110000000000000, 0xC120000000000000))
(0.0, 1.0, -2.0)
>>> _parse_real8(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: REAL8
>>> _parse_real8(b'abcd') # not divisible by 8
Traceback (most recent call last):
...
IncorrectDataSize: REAL8
"""
data_len = len(data)
if not data_len or (data_len % 8):
raise exceptions.IncorrectDataSize('REAL8')
ints = struct.unpack('>%dQ' % (data_len//8), data)
return tuple(_int_to_real(n) for n in ints)
def _parse_ascii(data):
r"""
Parse ASCII data type.
>>> _parse_ascii(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: ASCII
>>> _parse_ascii(b'abcde') == b'abcde'
True
>>> _parse_ascii(b'abcde\0') == b'abcde' # strips trailing NUL
True
"""
if not len(data):
raise exceptions.IncorrectDataSize('ASCII')
# XXX cross-version compatibility
if data[-1:] == b'\0':
return data[:-1]
return data
_PARSE_FUNCS = {
types.NODATA: _parse_nodata,
types.BITARRAY: _parse_bitarray,
types.INT2: _parse_int2,
types.INT4: _parse_int4,
types.REAL8: _parse_real8,
types.ASCII: _parse_ascii
}
def _pack_nodata(data):
"""
Pack NODATA tag data. Should always return empty string::
>>> packed = _pack_nodata([])
>>> packed == b''
True
>>> len(packed)
0
"""
return b''
def _pack_bitarray(data):
"""
Pack BITARRAY tag data.
>>> packed = _pack_bitarray(123)
>>> packed == struct.pack('>H', 123)
True
>>> len(packed)
2
"""
return struct.pack('>H', data)
def _pack_int2(data):
"""
Pack INT2 tag data.
>>> _pack_int2([1, 2, -3]) == struct.pack('>3h', 1, 2, -3)
True
>>> packed = _pack_int2((1, 2, 3))
>>> packed == struct.pack('>3h', 1, 2, 3)
True
>>> len(packed)
6
"""
size = len(data)
return struct.pack('>{0}h'.format(size), *data)
def _pack_int4(data):
"""
Pack INT4 tag data.
>>> _pack_int4([1, 2, -3]) == struct.pack('>3l', 1, 2, -3)
True
>>> packed = _pack_int4((1, 2, 3))
>>> packed == struct.pack('>3l', 1, 2, 3)
True
>>> len(packed)
12
"""
size = len(data)
return struct.pack('>{0}l'.format(size), *data)
def _real_to_int(fnum):
"""
Convert REAL8 from Python real to internal integer representation.
>>> '0x%016x' % _real_to_int(0.0)
'0x0000000000000000'
>>> print(_int_to_real(_real_to_int(1.0)))
1.0
>>> print(_int_to_real(_real_to_int(-2.0)))
-2.0
>>> print(_int_to_real(_real_to_int(1e-9)))
1e-09
"""
# first convert number to IEEE double and split it in parts
(ieee,) = struct.unpack('=Q', struct.pack('=d', fnum))
sign = ieee & 0x8000000000000000
ieee_exp = (ieee >> 52) & 0x7ff
ieee_mant = ieee & 0xfffffffffffff
if ieee_exp == 0:
# zero or denormals
# TODO maybe handle denormals
return 0
# substract exponent bias
unb_ieee_exp = ieee_exp - 1023
# add leading one and move to GDSII position
ieee_mant_full = (ieee_mant + 0x10000000000000) << 3
# convert exponent to 16-based, +1 for differences in presentation
# of mantissa (1.xxxx in EEEE and 0.1xxxxx in GDSII
exp16, rest = divmod(unb_ieee_exp + 1, 4)
# compensate exponent converion
if rest:
rest = 4 - rest
exp16 += 1
ieee_mant_comp = ieee_mant_full >> rest
# add GDSII exponent bias
exp16_biased = exp16 + 64
# try to fit everything
if exp16_biased < -14:
return 0 # number is too small. FIXME is it possible?
elif exp16_biased < 0:
ieee_mant_comp = ieee_mant_comp >> (exp16_biased * 4)
exp16_biased = 0
elif exp16_biased > 0x7f:
raise exceptions.FormatError('number is to big for REAL8')
return sign | (exp16_biased << 56) | ieee_mant_comp
def _pack_real8(data):
"""
Pack REAL8 tag data.
>>> packed = _pack_real8([0, 1, -1, 0.5, 1e-9])
>>> len(packed)
40
>>> list(map(str, _parse_real8(packed)))
['0.0', '1.0', '-1.0', '0.5', '1e-09']
"""
size = len(data)
return struct.pack('>{0}Q'.format(size), *[_real_to_int(num) for num in data])
def _pack_ascii(data):
r"""
Pack ASCII tag data.
>>> _pack_ascii(b'abcd') == b'abcd'
True
>>> _pack_ascii(b'abc') == b'abc\0'
True
"""
size = len(data)
if size % 2:
return data + b'\0'
return data
_PACK_FUNCS = {
types.NODATA: _pack_nodata,
types.BITARRAY: _pack_bitarray,
types.INT2: _pack_int2,
types.INT4: _pack_int4,
types.REAL8: _pack_real8,
types.ASCII: _pack_ascii
}
class Record(object):
"""
Class for representing a GDSII record with attached data.
Example::
>>> r = Record(tags.STRNAME, 'my_structure')
>>> '%04x' % r.tag
'0606'
>>> r.tag_name
'STRNAME'
>>> r.tag_type
6
>>> r.tag_type_name
'ASCII'
>>> r.data
'my_structure'
>>> r = Record(0xffff, 'xxx') # Unknown tag type
>>> r.tag_name
'0xffff'
>>> r.tag_type_name
'0xff'
"""
__slots__ = ['tag', 'data']
def __init__(self, tag, data=None, points=None, times=None, acls=None):
"""Initialize with tag and parsed data."""
self.tag = tag
if data is not None:
self.data = data
elif points is not None:
new_data = []
# TODO make it faster
for point in points:
new_data.append(point[0])
new_data.append(point[1])
self.data = new_data
elif times is not None:
mod_time = times[0]
acc_time = times[1]
self.data = (
mod_time.year - 1900,
mod_time.month,
mod_time.day,
mod_time.hour,
mod_time.minute,
mod_time.second,
acc_time.year - 1900,
acc_time.month,
acc_time.day,
acc_time.hour,
acc_time.minute,
acc_time.second
)
elif acls is not None:
new_data = []
for acl in acls:
new_data.extend(acl)
self.data = new_data
else:
self.data = None
def check_tag(self, tag):
"""
Checks if current record has the same tag as the given one.
Raises :exc:`MissingRecord` exception otherwise. For example::
>>> rec = Record(tags.STRNAME, b'struct')
>>> rec.check_tag(tags.STRNAME)
>>> rec.check_tag(tags.DATATYPE)
Traceback (most recent call last):
...
MissingRecord: Wanted: 3586, got: STRNAME
"""
if self.tag != tag:
raise exceptions.MissingRecord('Wanted: %s, got: %s'%(tag, self.tag_name))
def check_size(self, size):
"""
Checks if data size equals to the given size.
Raises :exc:`DataSizeError` otherwise. For example::
>>> rec = Record(tags.DATATYPE, (0,))
>>> rec.check_size(1)
>>> rec.check_size(5)
Traceback (most recent call last):
...
DataSizeError: 3586
"""
if len(self.data) != size:
raise exceptions.DataSizeError(self.tag)
@classmethod
def read(cls, stream):
"""
Read a GDSII record from file.
:param stream: GDS file opened for reading in binary mode
:returns: a new :class:`Record` instance
:raises: :exc:`UnsupportedTagType` if data cannot be parsed
:raises: :exc:`EndOfFileError` if end of file is reached
"""
header = stream.read(4)
if not header or len(header) != 4:
raise exceptions.EndOfFileError
data_size, tag = _RECORD_HEADER_FMT.unpack(header)
if data_size < 4:
raise exceptions.IncorrectDataSize('data size is too small')
if data_size % 2:
raise exceptions.IncorrectDataSize('data size is odd')
data_size -= 4 # substract header size
data = stream.read(data_size)
if len(data) != data_size:
raise exceptions.EndOfFileError
tag_type = tags.type_of_tag(tag)
try:
parse_func = _PARSE_FUNCS[tag_type]
except KeyError:
raise exceptions.UnsupportedTagType(tag_type)
return cls(tag, parse_func(data))
def save(self, stream):
"""
Save record to a GDS file.
:param stream: file opened for writing in binary mode
:raises: :exc:`UnsupportedTagType` if tag type is not supported
:raises: :exc:`FormatError` on incorrect data sizes, etc
:raises: whatever :func:`struct.pack` can raise
"""
tag_type = self.tag_type
try:
pack_func = _PACK_FUNCS[tag_type]
except KeyError:
raise exceptions.UnsupportedTagType(tag_type)
packed_data = pack_func(self.data)
record_size = len(packed_data) + 4
if record_size > 0xFFFF:
raise exceptions.FormatError('data size is too big')
header = _RECORD_HEADER_FMT.pack(record_size, self.tag)
stream.write(header)
stream.write(packed_data)
@property
def tag_name(self):
"""Tag name, if known, otherwise tag ID formatted as hex number."""
if self.tag in tags.REV_DICT:
return tags.REV_DICT[self.tag]
return '0x%04x' % self.tag
@property
def tag_type(self):
"""Tag data type ID."""
return tags.type_of_tag(self.tag)
@property
def tag_type_name(self):
"""Tag data type name, if known, and formatted number otherwise."""
tag_type = tags.type_of_tag(self.tag)
if tag_type in types.REV_DICT:
return types.REV_DICT[tag_type]
return '0x%02x' % tag_type
@property
def points(self):
"""
Convert data to list of points. Useful for :const:`XY` record.
Raises :exc:`DataSizeError` if data size is incorrect.
For example::
>>> r = Record(tags.XY, [0, 1, 2, 3])
>>> r.points
[(0, 1), (2, 3)]
>>> r = Record(tags.XY, []) # not allowed
>>> r.points
Traceback (most recent call last):
...
DataSizeError: 4099
>>> r = Record(tags.XY, [1, 2, 3]) # odd number of coordinates
>>> r.points
Traceback (most recent call last):
...
DataSizeError: 4099
"""
data_size = len(self.data)
if not data_size or (data_size % 2):
raise exceptions.DataSizeError(self.tag)
return [(self.data[i], self.data[i+1]) for i in range(0, data_size, 2)]
@property
def times(self):
"""
Convert data to tuple ``(modification time, access time)``.
Useful for :const:`BGNLIB` and :const:`BGNSTR`.
>>> r = Record(tags.BGNLIB, [100, 1, 1, 1, 2, 3, 110, 8, 14, 21, 10, 35])
>>> print(r.times[0].isoformat())
2000-01-01T01:02:03
>>> print(r.times[1].isoformat())
2010-08-14T21:10:35
>>> r = Record(tags.BGNLIB, [100, 1, 1, 1, 2, 3]) # wrong data length
>>> r.times
Traceback (most recent call last):
...
DataSizeError: 258
"""
if len(self.data) != 12:
raise exceptions.DataSizeError(self.tag)
return (datetime(self.data[0]+1900, *self.data[1:6]),
datetime(self.data[6]+1900, *self.data[7:12]))
@property
def acls(self):
"""
Convert data to list of acls ``(GID, UID, ACCESS)``.
Useful for :const:`LIBSECUR`.
>>> r = Record(tags.LIBSECUR, [1, 2, 3, 4, 5, 6])
>>> r.acls
[(1, 2, 3), (4, 5, 6)]
>>> r = Record(tags.LIBSECUR, [1, 2, 3, 4]) # wrong data size
>>> r.acls
Traceback (most recent call last):
...
DataSizeError: 15106
"""
if len(self.data) % 3:
raise exceptions.DataSizeError(self.tag)
return list(zip(self.data[::3], self.data[1::3], self.data[2::3]))
@classmethod
def iterate(cls, stream):
"""
Generator function for iterating over all records in a GDSII file.
Yields :class:`Record` objects.
:param stream: GDS file opened for reading in binary mode
"""
last = False
while not last:
rec = cls.read(stream)
if rec.tag == tags.ENDLIB:
last = True
yield rec
class Reader(object):
"""Class for buffered reading of Records"""
__slots__ = ('current', 'stream')
def __init__(self, stream):
self.stream = stream
def read_next(self):
"""Read and return next record from stream."""
self.current = Record.read(self.stream)
return self.current
if __name__ == '__main__':
import doctest
doctest.testmod(optionflags=doctest.IGNORE_EXCEPTION_DETAIL)