|
|
|
"""
|
|
|
|
This module contains all 'record' or 'block'-level datastructures and their
|
|
|
|
associated writing and parsing code, as well as a few helper functions.
|
|
|
|
|
|
|
|
Additionally, this module contains definitions for the record-level modal
|
|
|
|
variables (stored in the Modals class).
|
|
|
|
|
|
|
|
Higher-level code (e.g. monitoring for combinations of records with
|
|
|
|
implicit and explicit references, code for deciding which record type to
|
|
|
|
parse, or code for dealing with nested records in a CBlock) should live
|
|
|
|
in main.py instead.
|
|
|
|
"""
|
|
|
|
from abc import ABCMeta, abstractmethod
|
|
|
|
from typing import List, Dict, Tuple
|
|
|
|
import copy
|
|
|
|
import math
|
|
|
|
import zlib
|
|
|
|
import io
|
|
|
|
import logging
|
|
|
|
import pprint
|
|
|
|
|
|
|
|
from .basic import AString, NString, repetition_t, property_value_t, real_t, \
|
|
|
|
ReuseRepetition, OffsetTable, Validation, read_point_list, read_property_value, \
|
|
|
|
read_bstring, read_uint, read_sint, read_real, read_repetition, read_interval, \
|
|
|
|
write_bstring, write_uint, write_sint, write_real, write_interval, write_point_list, \
|
|
|
|
write_property_value, read_bool_byte, write_bool_byte, read_byte, write_byte, \
|
|
|
|
InvalidDataError, PathExtensionScheme
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
'''
|
|
|
|
Type definitions
|
|
|
|
'''
|
|
|
|
geometry_t = 'Text' or 'Rectangle' or 'Polygon' or 'Path' or 'Trapezoid' or \
|
|
|
|
'CTrapezoid' or 'Circle' or 'XElement' or 'XGeometry'
|
|
|
|
pathextension_t = Tuple['PathExtensionScheme' or int]
|
|
|
|
|
|
|
|
|
|
|
|
class Modals:
|
|
|
|
"""
|
|
|
|
Modal variables, used to store data about previously-written ori
|
|
|
|
-read records.
|
|
|
|
"""
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
placement_x = 0 # type: int
|
|
|
|
placement_y = 0 # type: int
|
|
|
|
placement_cell = None # type: int or NString or None
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
text_layer = None # type: int or None
|
|
|
|
text_datatype = None # type: int or None
|
|
|
|
text_x = 0 # type: int
|
|
|
|
text_y = 0 # type: int
|
|
|
|
text_string = None # type: AString or int or None
|
|
|
|
geometry_x = 0 # type: int
|
|
|
|
geometry_y = 0 # type: int
|
|
|
|
xy_relative = False # type: bool
|
|
|
|
geometry_w = None # type: int or None
|
|
|
|
geometry_h = None # type: int or None
|
|
|
|
polygon_point_list = None # type: List[List[int]] or None
|
|
|
|
path_halfwidth = None # type: int or None
|
|
|
|
path_point_list = None # type: List[List[int]] or None
|
|
|
|
path_extension_start = None # type: pathextension_t or None
|
|
|
|
path_extension_end = None # type: pathextension_t or None
|
|
|
|
ctrapezoid_type = None # type: int or None
|
|
|
|
circle_radius = None # type: int or None
|
|
|
|
property_value_list = None # type: List[property_value_t] or None
|
|
|
|
property_name = None # type: int or NString or None
|
|
|
|
property_is_standard = None # type: bool or None
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.reset()
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
"""
|
|
|
|
Resets all modal variables to their default values.
|
|
|
|
Default values are:
|
|
|
|
0 for placement_{x,y}, text_{x,y}, geometry_{x,y}
|
|
|
|
False for xy_relative
|
|
|
|
Undefined (None) for all others
|
|
|
|
"""
|
|
|
|
self.repetition = None
|
|
|
|
self.placement_x = 0
|
|
|
|
self.placement_y = 0
|
|
|
|
self.placement_cell = None
|
|
|
|
self.layer = None
|
|
|
|
self.datatype = None
|
|
|
|
self.text_layer = None
|
|
|
|
self.text_datatype = None
|
|
|
|
self.text_x = 0
|
|
|
|
self.text_y = 0
|
|
|
|
self.text_string = None
|
|
|
|
self.geometry_x = 0
|
|
|
|
self.geometry_y = 0
|
|
|
|
self.xy_relative = False
|
|
|
|
self.geometry_w = None
|
|
|
|
self.geometry_h = None
|
|
|
|
self.polygon_point_list = None
|
|
|
|
self.path_halfwidth = None
|
|
|
|
self.path_point_list = None
|
|
|
|
self.path_extension_start = None
|
|
|
|
self.path_extension_end = None
|
|
|
|
self.ctrapezoid_type = None
|
|
|
|
self.circle_radius = None
|
|
|
|
self.property_value_list = None
|
|
|
|
self.property_name = None
|
|
|
|
self.property_is_standard = None
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
Records
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
class Record(metaclass=ABCMeta):
|
|
|
|
"""
|
|
|
|
Common interface for records.
|
|
|
|
"""
|
|
|
|
@abstractmethod
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
"""
|
|
|
|
Copy all defined values from this record into the modal variables.
|
|
|
|
Fill all undefined values in this record from the modal variables.
|
|
|
|
|
|
|
|
:param modals: Modal variables to merge with.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
"""
|
|
|
|
Check all defined values in this record against those in the
|
|
|
|
modal variables. If any values are equal, remove them from
|
|
|
|
the record and indicate that the modal variables should be
|
|
|
|
used instead. Update the modal variables using the remaining
|
|
|
|
(unequal) values.
|
|
|
|
|
|
|
|
:param modals: Modal variables to deduplicate with.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
@abstractmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Record':
|
|
|
|
"""
|
|
|
|
Read a record of this type from a stream.
|
|
|
|
This function does not merge with modal variables.
|
|
|
|
|
|
|
|
:param stream: Stream to read from.
|
|
|
|
:param record_id: Record id of the record to read. The
|
|
|
|
record id is often used to specify which variant
|
|
|
|
of the record is stored.
|
|
|
|
:return: The record that was read.
|
|
|
|
:raises: InvalidDataError if the record is malformed.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
"""
|
|
|
|
Write this record to a stream as-is.
|
|
|
|
This function does not merge or deduplicate with modal variables.
|
|
|
|
|
|
|
|
:param stream: Stream to write to.
|
|
|
|
:return: Number of bytes written.
|
|
|
|
:raises: InvalidDataError if the record contains invalid data.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int:
|
|
|
|
"""
|
|
|
|
Run .deduplicate_with_modals() and then .write() to the stream.
|
|
|
|
|
|
|
|
:param stream: Stream to write to.
|
|
|
|
:param modals: Modal variables to merge with.
|
|
|
|
:return: Number of bytes written
|
|
|
|
:raises: InvalidDataError if the record contains invalid data.
|
|
|
|
"""
|
|
|
|
# TODO logging
|
|
|
|
#print(type(self), stream.tell())
|
|
|
|
self.deduplicate_with_modals(modals)
|
|
|
|
return self.write(stream)
|
|
|
|
|
|
|
|
def copy(self) -> 'Record':
|
|
|
|
"""
|
|
|
|
Perform a deep copy of this record.
|
|
|
|
|
|
|
|
:return: A deep copy of this record.
|
|
|
|
"""
|
|
|
|
return copy.deepcopy(self)
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
return '{}: {}'.format(self.__class__, pprint.pformat(self.__dict__))
|
|
|
|
|
|
|
|
|
|
|
|
def read_refname(stream: io.BufferedIOBase,
|
|
|
|
is_present: bool,
|
|
|
|
is_reference: bool
|
|
|
|
) -> None or int or NString:
|
|
|
|
"""
|
|
|
|
Helper function for reading a possibly-absent, possibly-referenced NString.
|
|
|
|
|
|
|
|
:param stream: Stream to read from.
|
|
|
|
:param is_present: If False, read nothing and return None
|
|
|
|
:param is_reference: If True, read a uint (reference id),
|
|
|
|
otherwise read an NString.
|
|
|
|
:return: None, reference id, or NString
|
|
|
|
"""
|
|
|
|
if not is_present:
|
|
|
|
return None
|
|
|
|
elif is_reference:
|
|
|
|
return read_uint(stream)
|
|
|
|
else:
|
|
|
|
return NString.read(stream)
|
|
|
|
|
|
|
|
|
|
|
|
def read_refstring(stream: io.BufferedIOBase,
|
|
|
|
is_present: bool,
|
|
|
|
is_reference: bool
|
|
|
|
) -> None or int or AString:
|
|
|
|
"""
|
|
|
|
Helper function for reading a possibly-absent, possibly-referenced AString.
|
|
|
|
|
|
|
|
:param stream: Stream to read from.
|
|
|
|
:param is_present: If False, read nothing and return None
|
|
|
|
:param is_reference: If True, read a uint (reference id),
|
|
|
|
otherwise read an AString.
|
|
|
|
:return: None, reference id, or AString
|
|
|
|
"""
|
|
|
|
if not is_present:
|
|
|
|
return None
|
|
|
|
elif is_reference:
|
|
|
|
return read_uint(stream)
|
|
|
|
else:
|
|
|
|
return AString.read(stream)
|
|
|
|
|
|
|
|
|
|
|
|
class Pad(Record):
|
|
|
|
"""
|
|
|
|
Pad record (ID 0)
|
|
|
|
"""
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Pad':
|
|
|
|
if record_id != 0:
|
|
|
|
raise InvalidDataError('Invalid record id for Pad '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
record = Pad()
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
return write_uint(stream, 0)
|
|
|
|
|
|
|
|
|
|
|
|
class XYMode(Record):
|
|
|
|
"""
|
|
|
|
XYMode record (ID 15, 16)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.relative (bool, default False)
|
|
|
|
"""
|
|
|
|
relative = False # type: bool
|
|
|
|
|
|
|
|
@property
|
|
|
|
def absolute(self) -> bool:
|
|
|
|
return not relative
|
|
|
|
|
|
|
|
@absolute.setter
|
|
|
|
def absolute(self, b: bool):
|
|
|
|
self.relative = not b
|
|
|
|
|
|
|
|
def __init__(self, relative: bool):
|
|
|
|
"""
|
|
|
|
:param relative: True if the mode is 'relative', False if 'absolute'.
|
|
|
|
"""
|
|
|
|
self.relative = relative
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.xy_relative = self.relative
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'XYMode':
|
|
|
|
if record_id not in (15, 16):
|
|
|
|
raise InvalidDataError('Invalid record id for XYMode')
|
|
|
|
record = XYMode(record_id == 16)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
return write_uint(stream, 15 + self.relative)
|
|
|
|
|
|
|
|
|
|
|
|
class Start(Record):
|
|
|
|
"""
|
|
|
|
Start Record (ID 1)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.version (AString, "1.0")
|
|
|
|
.unit (positive real number, grid steps per micron)
|
|
|
|
.offset_table (OffsetTable or None, if None then table must be
|
|
|
|
placed in the End record)
|
|
|
|
"""
|
|
|
|
version = None # type: AString
|
|
|
|
unit = None # type: real_t
|
|
|
|
offset_table = None # type: OffsetTable
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
unit: real_t,
|
|
|
|
version: AString or str = None,
|
|
|
|
offset_table: OffsetTable = None):
|
|
|
|
"""
|
|
|
|
:param unit: Grid steps per micron (positive real number)
|
|
|
|
:param version: Version string, default "1.0"
|
|
|
|
:param offset_table: OffsetTable for the file, or None to place
|
|
|
|
it in the End record instead.
|
|
|
|
"""
|
|
|
|
if unit <= 0:
|
|
|
|
raise InvalidDataError('Non-positive unit: {}'.format(unit))
|
|
|
|
if math.isnan(unit):
|
|
|
|
raise InvalidDataError('NaN unit')
|
|
|
|
if math.isinf(unit):
|
|
|
|
raise InvalidDataError('Non-finite unit')
|
|
|
|
self.unit = unit
|
|
|
|
|
|
|
|
if version is None:
|
|
|
|
version = AString('1.0')
|
|
|
|
if isinstance(version, AString):
|
|
|
|
self.version = version
|
|
|
|
else:
|
|
|
|
self.version = AString(version)
|
|
|
|
|
|
|
|
if self.version.string != '1.0':
|
|
|
|
raise InvalidDataError('Invalid version string, '
|
|
|
|
'only "1.0" is allowed: '
|
|
|
|
+ str(self.version.string))
|
|
|
|
self.offset_table = offset_table
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Start':
|
|
|
|
if record_id != 1:
|
|
|
|
raise InvalidDataError('Invalid record id for Start: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
version = AString.read(stream)
|
|
|
|
unit = read_real(stream)
|
|
|
|
has_offset_table = read_uint(stream) == 0
|
|
|
|
if has_offset_table:
|
|
|
|
offset_table = OffsetTable.read(stream)
|
|
|
|
else:
|
|
|
|
offset_table = None
|
|
|
|
record = Start(unit, version, offset_table)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
size = write_uint(stream, 1)
|
|
|
|
size += self.version.write(stream)
|
|
|
|
size += write_real(stream, self.unit)
|
|
|
|
size += write_uint(stream, self.offset_table is None)
|
|
|
|
if self.offset_table is not None:
|
|
|
|
size += self.offset_table.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class End(Record):
|
|
|
|
"""
|
|
|
|
End record (ID 2)
|
|
|
|
|
|
|
|
The end record is always padded to a total length of 256 bytes.
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.offset_table (OffsetTable or None, None if offset table was
|
|
|
|
written into the Start record instead)
|
|
|
|
.validation (Validation object)
|
|
|
|
"""
|
|
|
|
offset_table = None # type: OffsetTable or None
|
|
|
|
validation = None # type: Validation
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
validation: Validation,
|
|
|
|
offset_table: OffsetTable = None):
|
|
|
|
"""
|
|
|
|
:param validation: Validation object for this file.
|
|
|
|
:param offset_table: OffsetTable, or None if the Start record
|
|
|
|
contained an OffsetTable. Default None.
|
|
|
|
"""
|
|
|
|
self.validation = validation
|
|
|
|
self.offset_table = offset_table
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase,
|
|
|
|
record_id: int,
|
|
|
|
has_offset_table: bool
|
|
|
|
) -> 'End':
|
|
|
|
if record_id != 2:
|
|
|
|
raise InvalidDataError('Invalid record id for End {}'.format(record_id))
|
|
|
|
if has_offset_table:
|
|
|
|
offset_table = OffsetTable.read(stream)
|
|
|
|
else:
|
|
|
|
offset_table = None
|
|
|
|
_padding_string = read_bstring(stream)
|
|
|
|
validation = Validation.read(stream)
|
|
|
|
record = End(validation, offset_table)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
size = write_uint(stream, 2)
|
|
|
|
if self.offset_table is not None:
|
|
|
|
size += self.offset_table.write(stream)
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
self.validation.write(buf)
|
|
|
|
validation_bytes = buf.getvalue()
|
|
|
|
|
|
|
|
pad_len = 256 - size - len(validation_bytes)
|
|
|
|
if pad_len > 0:
|
|
|
|
pad = [0x80] * (pad_len - 1) + [0x00]
|
|
|
|
stream.write(bytes(pad))
|
|
|
|
stream.write(validation_bytes)
|
|
|
|
return 256
|
|
|
|
|
|
|
|
|
|
|
|
class CBlock(Record):
|
|
|
|
"""
|
|
|
|
CBlock (Compressed Block) record (ID 34)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.compression_type (int, 0 for zlib)
|
|
|
|
.decompressed_byte_count (int)
|
|
|
|
.compressed_bytes (bytes)
|
|
|
|
"""
|
|
|
|
compression_type = None # type: int
|
|
|
|
decompressed_byte_count = None # type: int
|
|
|
|
compressed_bytes = None # type: bytes
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
compression_type: int,
|
|
|
|
decompressed_byte_count: int,
|
|
|
|
compressed_bytes: bytes):
|
|
|
|
"""
|
|
|
|
:param compression_type: 0 (zlib)
|
|
|
|
:param decompressed_byte_count: Number of bytes in the decompressed data.
|
|
|
|
:param compressed_bytes: The compressed data.
|
|
|
|
"""
|
|
|
|
if compression_type != 0:
|
|
|
|
raise InvalidDataError('CBlock: Invalid compression scheme '
|
|
|
|
'{}'.format(compression_type))
|
|
|
|
|
|
|
|
self.compression_type = compression_type
|
|
|
|
self.decompressed_byte_count = decompressed_byte_count
|
|
|
|
self.compressed_bytes = compressed_bytes
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'CBlock':
|
|
|
|
if record_id != 34:
|
|
|
|
raise InvalidDataError('Invalid record id for CBlock: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
compression_type = read_uint(stream)
|
|
|
|
decompressed_count = read_uint(stream)
|
|
|
|
compressed_bytes = read_bstring(stream)
|
|
|
|
return CBlock(compression_type, decompressed_count, compressed_bytes)
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
size = write_uint(stream, 34)
|
|
|
|
size += write_uint(stream, self.compression_type)
|
|
|
|
size += write_uint(stream, self.decompressed_byte_count)
|
|
|
|
size += write_bstring(stream, self.compressed_bytes)
|
|
|
|
return size
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_decompressed(decompressed_bytes: bytes,
|
|
|
|
compression_type: int = 0,
|
|
|
|
compression_args: Dict = None
|
|
|
|
) -> 'CBlock':
|
|
|
|
"""
|
|
|
|
Create a CBlock record from uncompressed data.
|
|
|
|
|
|
|
|
:param decompressed_bytes: Uncompressed data (one or more non-CBlock records)
|
|
|
|
:param compression_type: Compression type (0: zlib). Default 0
|
|
|
|
:param compression_args Passed as kwargs to zlib.compressobj(). Default {}.
|
|
|
|
:return: CBlock object constructed from the data.
|
|
|
|
:raises: InvalidDataError if invalid compression_type.
|
|
|
|
"""
|
|
|
|
if compression_args is None:
|
|
|
|
compression_args = {}
|
|
|
|
|
|
|
|
if compression_type == 0:
|
|
|
|
count = len(decompressed_bytes)
|
|
|
|
compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS, **compression_args)
|
|
|
|
compressed_bytes = compressor.compress(decompressed_bytes) + \
|
|
|
|
compressor.flush()
|
|
|
|
else:
|
|
|
|
raise InvalidDataError('Unknown compression type: '
|
|
|
|
'{}'.format(compression_type))
|
|
|
|
|
|
|
|
return CBlock(compression_type, count, compressed_bytes)
|
|
|
|
|
|
|
|
def decompress(self, decompression_args: Dict = None) -> bytes:
|
|
|
|
"""
|
|
|
|
Decompress the contents of this CBlock.
|
|
|
|
|
|
|
|
:param decompression_args: Passed as kwargs to zlib.decompressobj().
|
|
|
|
:return: Decompressed bytes object.
|
|
|
|
:raises: InvalidDataError if data is malformed or compression type is
|
|
|
|
unknonwn.
|
|
|
|
"""
|
|
|
|
if decompression_args is None:
|
|
|
|
decompression_args = {}
|
|
|
|
if self.compression_type == 0:
|
|
|
|
decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS, **decompression_args)
|
|
|
|
decompressed_bytes = decompressor.decompress(self.compressed_bytes) + \
|
|
|
|
decompressor.flush()
|
|
|
|
if len(decompressed_bytes) != self.decompressed_byte_count:
|
|
|
|
raise InvalidDataError('Decompressed data length does not match!')
|
|
|
|
else:
|
|
|
|
raise InvalidDataError('Unknown compression type: '
|
|
|
|
'{}'.format(self.compression_type))
|
|
|
|
return decompressed_bytes
|
|
|
|
|
|
|
|
|
|
|
|
class CellName(Record):
|
|
|
|
"""
|
|
|
|
CellName record (ID 3, 4)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.nstring (NString)
|
|
|
|
.reference_number (int or None)
|
|
|
|
"""
|
|
|
|
nstring = None # type: NString
|
|
|
|
reference_number = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
nstring: NString or str,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
|
|
|
:param nstring: The contained string.
|
|
|
|
:param reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
|
|
|
"""
|
|
|
|
if isinstance(nstring, NString):
|
|
|
|
self.nstring = nstring
|
|
|
|
else:
|
|
|
|
self.nstring = NString(nstring)
|
|
|
|
self.reference_number = reference_number
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'CellName':
|
|
|
|
if record_id not in (3, 4):
|
|
|
|
raise InvalidDataError('Invalid record id for CellName '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
nstring = NString.read(stream)
|
|
|
|
if record_id == 4:
|
|
|
|
reference_number = read_uint(stream)
|
|
|
|
else:
|
|
|
|
reference_number = None
|
|
|
|
record = CellName(nstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 3 + (self.reference_number is not None)
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += self.nstring.write(stream)
|
|
|
|
if self.reference_number is not None:
|
|
|
|
size += write_uint(stream, self.reference_number)
|
|
|
|
return size
|
|
|
|
|
|
|
|
class PropName(Record):
|
|
|
|
"""
|
|
|
|
PropName record (ID 7, 8)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.nstring (NString)
|
|
|
|
.reference_number (int or None)
|
|
|
|
"""
|
|
|
|
nstring = None # type: NString
|
|
|
|
reference_number = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
nstring: NString or str,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
|
|
|
:param nstring: The contained string.
|
|
|
|
:param reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
|
|
|
"""
|
|
|
|
if isinstance(nstring, NString):
|
|
|
|
self.nstring = nstring
|
|
|
|
else:
|
|
|
|
self.nstring = NString(nstring)
|
|
|
|
self.reference_number = reference_number
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'PropName':
|
|
|
|
if record_id not in (7, 8):
|
|
|
|
raise InvalidDataError('Invalid record id for PropName '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
nstring = NString.read(stream)
|
|
|
|
if record_id == 8:
|
|
|
|
reference_number = read_uint(stream)
|
|
|
|
else:
|
|
|
|
reference_number = None
|
|
|
|
record = PropName(nstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 7 + (self.reference_number is not None)
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += self.nstring.write(stream)
|
|
|
|
if self.reference_number is not None:
|
|
|
|
size += write_uint(stream, self.reference_number)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class TextString(Record):
|
|
|
|
"""
|
|
|
|
TextString record (ID 5, 6)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.astring (AString)
|
|
|
|
.reference_number (int or None)
|
|
|
|
"""
|
|
|
|
astring = None # type: AString
|
|
|
|
reference_number = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
string: AString or str,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
|
|
|
:param string: The contained string.
|
|
|
|
:param reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
|
|
|
"""
|
|
|
|
if isinstance(string, AString):
|
|
|
|
self.astring = string
|
|
|
|
else:
|
|
|
|
self.astring = AString(string)
|
|
|
|
self.reference_number = reference_number
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'TextString':
|
|
|
|
if record_id not in (5, 6):
|
|
|
|
raise InvalidDataError('Invalid record id for TextString: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
astring = AString.read(stream)
|
|
|
|
if record_id == 6:
|
|
|
|
reference_number = read_uint(stream)
|
|
|
|
else:
|
|
|
|
reference_number = None
|
|
|
|
record = TextString(astring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 5 + (self.reference_number is not None)
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += self.astring.write(stream)
|
|
|
|
if self.reference_number is not None:
|
|
|
|
size += write_uint(stream, self.reference_number)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class PropString(Record):
|
|
|
|
"""
|
|
|
|
PropString record (ID 9, 10)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.astring (AString)
|
|
|
|
.reference_number (int or None)
|
|
|
|
"""
|
|
|
|
astring = None # type: AString
|
|
|
|
reference_number = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
string: AString or str,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
|
|
|
:param string: The contained string.
|
|
|
|
:param reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
|
|
|
"""
|
|
|
|
if isinstance(string, AString):
|
|
|
|
self.astring = string
|
|
|
|
else:
|
|
|
|
self.astring = AString(string)
|
|
|
|
self.reference_number = reference_number
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'PropString':
|
|
|
|
if record_id not in (9, 10):
|
|
|
|
raise InvalidDataError('Invalid record id for PropString: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
astring = AString.read(stream)
|
|
|
|
if record_id == 10:
|
|
|
|
reference_number = read_uint(stream)
|
|
|
|
else:
|
|
|
|
reference_number = None
|
|
|
|
record = PropString(astring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 9 + (self.reference_number is not None)
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += self.astring.write(stream)
|
|
|
|
if self.reference_number is not None:
|
|
|
|
size += write_uint(stream, self.reference_number)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class LayerName(Record):
|
|
|
|
"""
|
|
|
|
LayerName record (ID 11, 12)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.nstring (NString)
|
|
|
|
.layer_interval (Tuple, (int or None, int or None),
|
|
|
|
bounds on the interval)
|
|
|
|
.type_interval (Tuple, (int or None, int or None),
|
|
|
|
bounds on the interval)
|
|
|
|
.is_textlayer (bool)
|
|
|
|
"""
|
|
|
|
nstring = None # type: NString,
|
|
|
|
layer_interval = None # type: Tuple
|
|
|
|
type_interval = None # type: Tuple
|
|
|
|
is_textlayer = None # type: bool
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
nstring: NString or str,
|
|
|
|
layer_interval: Tuple,
|
|
|
|
type_interval: Tuple,
|
|
|
|
is_textlayer: bool):
|
|
|
|
"""
|
|
|
|
:param nstring: The layer name.
|
|
|
|
:param layer_interval: Tuple (int or None, int or None) giving bounds
|
|
|
|
(or lack of thereof) on the layer number.
|
|
|
|
:param type_interval: Tuple (int or None, int or None) giving bounds
|
|
|
|
(or lack of thereof) on the type number.
|
|
|
|
:param is_textlayer: True if the layer is a text layer.
|
|
|
|
"""
|
|
|
|
if isinstance(nstring, NString):
|
|
|
|
self.nstring = nstring
|
|
|
|
else:
|
|
|
|
self.nstring = NString(nstring)
|
|
|
|
self.layer_interval = layer_interval
|
|
|
|
self.type_interval = type_interval
|
|
|
|
self.is_textlayer = is_textlayer
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'LayerName':
|
|
|
|
if record_id not in (11, 12):
|
|
|
|
raise InvalidDataError('Invalid record id for LayerName: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
is_textlayer = (record_id == 12)
|
|
|
|
nstring = AString.read(stream)
|
|
|
|
layer_interval = read_interval(stream)
|
|
|
|
type_interval = read_interval(stream)
|
|
|
|
record = LayerName(nstring, layer_interval, type_interval, is_textlayer)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 11 + self.is_textlayer
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += self.nstring.write(stream)
|
|
|
|
size += write_interval(stream, *self.layer_interval)
|
|
|
|
size += write_interval(stream, *self.type_interval)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Property(Record):
|
|
|
|
"""
|
|
|
|
LayerName record (ID 28, 29)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.name (NString or int or None,
|
|
|
|
int is an explicit reference,
|
|
|
|
None is a flag to use Modal)
|
|
|
|
.values (List of property values or None)
|
|
|
|
.is_standard (bool, whether this is a standard property)
|
|
|
|
"""
|
|
|
|
name = None # type: NString or int or None,
|
|
|
|
values = None # type: List[property_value_t] or None
|
|
|
|
is_standard = None # type: bool or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
name: NString or str or int = None,
|
|
|
|
values: List[property_value_t] = None,
|
|
|
|
is_standard: bool = None):
|
|
|
|
"""
|
|
|
|
:param name: Property name, reference number, or None (i.e. use modal)
|
|
|
|
Default None.
|
|
|
|
:param values: List of property values, or None (i.e. use modal)
|
|
|
|
Default None.
|
|
|
|
:param is_standard: True if this is a standard property. None to use modal.
|
|
|
|
Default None.
|
|
|
|
"""
|
|
|
|
if isinstance(name, str):
|
|
|
|
self.name = NString(name)
|
|
|
|
else:
|
|
|
|
self.name = name
|
|
|
|
self.values = values
|
|
|
|
self.is_standard = is_standard
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_field(self, 'name', modals, 'property_name')
|
|
|
|
adjust_field(self, 'values', modals, 'property_value_list')
|
|
|
|
adjust_field(self, 'is_standard', modals, 'property_is_standard')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_field(self, 'name', modals, 'property_name')
|
|
|
|
dedup_field(self, 'values', modals, 'property_value_list')
|
|
|
|
if self.values is None and self.name is None:
|
|
|
|
dedup_field(self, 'is_standard', modals, 'property_is_standard')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Property':
|
|
|
|
if record_id not in (28, 29):
|
|
|
|
raise InvalidDataError('Invalid record id for PropertyValue: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
if record_id == 29:
|
|
|
|
record = Property()
|
|
|
|
else:
|
|
|
|
byte = read_byte(stream) #UUUUVCNS
|
|
|
|
u = 0x0f & (byte >> 4)
|
|
|
|
v = 0x01 & (byte >> 3)
|
|
|
|
c = 0x01 & (byte >> 2)
|
|
|
|
n = 0x01 & (byte >> 1)
|
|
|
|
s = 0x01 & (byte >> 0)
|
|
|
|
|
|
|
|
name = read_refname(stream, c, n)
|
|
|
|
if v == 0:
|
|
|
|
if u < 0x0f:
|
|
|
|
value_count = u
|
|
|
|
else:
|
|
|
|
value_count = read_uint(stream)
|
|
|
|
values = [read_property_value(stream) for _ in range(value_count)]
|
|
|
|
else:
|
|
|
|
values = None
|
|
|
|
if u != 0:
|
|
|
|
raise InvalidDataError('Malformed property record header')
|
|
|
|
record = Property(name, values, s)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
if self.is_standard is None and self.values is None and self.name is None:
|
|
|
|
return write_uint(stream, 29)
|
|
|
|
else:
|
|
|
|
if self.is_standard is None:
|
|
|
|
raise InvalidDataError('Property has value or name, '
|
|
|
|
'but no is_standard flag!')
|
|
|
|
if self.values is not None:
|
|
|
|
value_count = len(self.values)
|
|
|
|
v = 0
|
|
|
|
if value_count >= 0x0f:
|
|
|
|
u = 0x0f
|
|
|
|
else:
|
|
|
|
u = value_count
|
|
|
|
else:
|
|
|
|
v = 1
|
|
|
|
u = 0
|
|
|
|
|
|
|
|
c = self.name is not None
|
|
|
|
n = c and isinstance(self.name, int)
|
|
|
|
s = self.is_standard
|
|
|
|
|
|
|
|
size = write_uint(stream, 28)
|
|
|
|
size += write_byte(stream, (u << 4) | (v << 3) | (c << 2) | (n << 1) | s)
|
|
|
|
if c:
|
|
|
|
if n:
|
|
|
|
size += write_uint(stream, self.name)
|
|
|
|
else:
|
|
|
|
size += self.name.write(stream)
|
|
|
|
if not v:
|
|
|
|
if u == 0x0f:
|
|
|
|
size += write_uint(stream, self.name)
|
|
|
|
size += sum(write_property_value(stream, p) for p in self.values)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class XName(Record):
|
|
|
|
"""
|
|
|
|
XName record (ID 30, 31)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.attribute (int)
|
|
|
|
.bstring (bytes)
|
|
|
|
.reference_number (int or None, None means to use implicity numbering)
|
|
|
|
"""
|
|
|
|
attribute = None # type: int
|
|
|
|
bstring = None # type: bytes
|
|
|
|
reference_number = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
attribute: int,
|
|
|
|
bstring: bytes,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
|
|
|
:param attribute: Attribute number.
|
|
|
|
:param bstring: Binary XName data.
|
|
|
|
:param reference_number: Reference number for this XName.
|
|
|
|
Default None (implicit).
|
|
|
|
"""
|
|
|
|
self.attribute = attribute
|
|
|
|
self.bstring = bstring
|
|
|
|
self.reference_number = reference_number
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'XName':
|
|
|
|
if record_id not in (30, 31):
|
|
|
|
raise InvalidDataError('Invalid record id for XName: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
attribute = read_uint(stream)
|
|
|
|
bstring = read_bstring(stream)
|
|
|
|
if record_id == 31:
|
|
|
|
reference_number = read_uint(stream)
|
|
|
|
else:
|
|
|
|
reference_number = None
|
|
|
|
record = XName(attribute, bstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
record_id = 30 + (self.reference_number is not None)
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += write_uint(stream, self.attribute)
|
|
|
|
size += write_bstring(stream, self.bstring)
|
|
|
|
if self.reference_number is not None:
|
|
|
|
size += write_uint(stream, self.reference_number)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class XElement(Record):
|
|
|
|
"""
|
|
|
|
XElement record (ID 32)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.attribute (int)
|
|
|
|
.bstring (bytes)
|
|
|
|
"""
|
|
|
|
attribute = None # type: int
|
|
|
|
bstring = None # type: bytes
|
|
|
|
|
|
|
|
def __init__(self, attribute: int, bstring: bytes):
|
|
|
|
"""
|
|
|
|
:param attribute: Attribute number.
|
|
|
|
:param bstring: Binary data for this XElement.
|
|
|
|
"""
|
|
|
|
self.attribute = attribute
|
|
|
|
self.bstring = bstring
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'XElement':
|
|
|
|
if record_id != 32:
|
|
|
|
raise InvalidDataError('Invalid record id for XElement: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
attribute = read_uint(stream)
|
|
|
|
bstring = read_bstring(stream)
|
|
|
|
record = XElement(attribute, bstring)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
size = write_uint(stream, 32)
|
|
|
|
size += write_uint(stream, self.attribute)
|
|
|
|
size += write_bstring(stream, self.bstring)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class XGeometry(Record):
|
|
|
|
"""
|
|
|
|
XGeometry record (ID 33)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.attribute (int)
|
|
|
|
.bstring (bytes)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means reuse modal)
|
|
|
|
.y (int or None, None means reuse modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
attribute = None # type: int
|
|
|
|
bstring = None # type: bytes
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
attribute: int,
|
|
|
|
bstring: bytes,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param attribute: Attribute number for this XGeometry.
|
|
|
|
:param bstring: Binary data for this XGeometry.
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.attribute = attribute
|
|
|
|
self.bstring = bstring
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'XGeometry':
|
|
|
|
if record_id != 33:
|
|
|
|
raise InvalidDataError('Invalid record id for XGeometry: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
z0, z1, z2, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
if z0 or z1 or z2:
|
|
|
|
raise InvalidDataError('Malformed XGeometry header')
|
|
|
|
attribute = read_uint(stream)
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
bstring = read_bstring(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
|
|
|
|
record = XGeometry(attribute, bstring, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 33)
|
|
|
|
size += write_bool_byte(stream, (0, 0, 0, x, y, r, d, l))
|
|
|
|
size += write_uint(stream, self.attribute)
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
size += write_bstring(stream, self.bstring)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Cell(Record):
|
|
|
|
"""
|
|
|
|
Cell record (ID 13, 14)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.name (NString or int specifying CellName reference number)
|
|
|
|
"""
|
|
|
|
name = None # type: int or NString
|
|
|
|
|
|
|
|
def __init__(self, name: int or NString):
|
|
|
|
"""
|
|
|
|
:param name: NString, or an int specifying a CellName reference number.
|
|
|
|
"""
|
|
|
|
self.name = name
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
modals.reset()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Cell':
|
|
|
|
if record_id == 13:
|
|
|
|
name = read_uint(stream)
|
|
|
|
elif record_id == 14:
|
|
|
|
name = NString.read(stream)
|
|
|
|
else:
|
|
|
|
raise InvalidDataError('Invalid record id for Cell: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
record = Cell(name)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
size = 0
|
|
|
|
if isinstance(self.name, int):
|
|
|
|
size += write_uint(stream, 13)
|
|
|
|
size += write_uint(stream, self.name)
|
|
|
|
else:
|
|
|
|
size += write_uint(stream, 14)
|
|
|
|
size += self.name.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Placement(Record):
|
|
|
|
"""
|
|
|
|
Placement record (ID 17, 18)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.attribute (int)
|
|
|
|
.name (NString, name or
|
|
|
|
int, CellName reference number or
|
|
|
|
None, reuse modal)
|
|
|
|
.magnification (real)
|
|
|
|
.angle (real, degrees counterclockwise)
|
|
|
|
.x (int or None, None means reuse modal)
|
|
|
|
.y (int or None, None means reuse modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
.flip (bool)
|
|
|
|
"""
|
|
|
|
name = None # type: NString or int or None
|
|
|
|
magnification = None # type: real_t or None
|
|
|
|
angle = None # type: real_t or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
flip = None # type: bool
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
flip: bool,
|
|
|
|
name: NString or str or int = None,
|
|
|
|
magnification: real_t = None,
|
|
|
|
angle: real_t = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param flip: Whether to perform reflection about the x-axis.
|
|
|
|
:param name: NString, an int specifying a CellName reference number,
|
|
|
|
or None (reuse modal).
|
|
|
|
:param magnification: Magnification factor. Default None (use modal).
|
|
|
|
:param angle: Rotation angle in degrees, counterclockwise.
|
|
|
|
Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
self.flip = flip
|
|
|
|
self.magnification = magnification
|
|
|
|
self.angle = angle
|
|
|
|
if isinstance(name, str):
|
|
|
|
self.name = NString(name)
|
|
|
|
else:
|
|
|
|
self.name = name
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'placement_x', 'placement_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'name', modals, 'placement_cell')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'placement_x', 'placement_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'name', modals, 'placement_cell')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Placement':
|
|
|
|
if record_id not in (17, 18):
|
|
|
|
raise InvalidDataError('Invalid record id for Placement: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
#CNXYRAAF (17) or CNXYRMAF (18)
|
|
|
|
c, n, x, y, r, ma0, ma1, flip = read_bool_byte(stream)
|
|
|
|
|
|
|
|
optional = {}
|
|
|
|
name = read_refname(stream, c, n)
|
|
|
|
if record_id == 17:
|
|
|
|
aa = (ma0 << 1) | ma1
|
|
|
|
optional['angle'] = aa * 90
|
|
|
|
elif record_id == 18:
|
|
|
|
m = ma0
|
|
|
|
a = ma1
|
|
|
|
if m:
|
|
|
|
optional['magnification'] = read_real(stream)
|
|
|
|
if a:
|
|
|
|
optional['angle'] = read_real(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
|
|
|
|
record = Placement(flip, name, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
c = self.name is not None
|
|
|
|
n = c and isinstance(self.name, int)
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
f = self.flip
|
|
|
|
|
|
|
|
if self.angle is not None and self.angle % 90 == 0 and \
|
|
|
|
self.magnification is None or self.magnification == 1:
|
|
|
|
aa = int((self.angle / 90) % 4)
|
|
|
|
bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f)
|
|
|
|
m = False
|
|
|
|
a = False
|
|
|
|
record_id = 17
|
|
|
|
else:
|
|
|
|
m = self.magnification is not None
|
|
|
|
a = self.angle is not None
|
|
|
|
bools = (c, n, x, y, r, m, a, f)
|
|
|
|
record_id = 18
|
|
|
|
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += write_bool_byte(stream, bools)
|
|
|
|
if c:
|
|
|
|
if n:
|
|
|
|
size += write_uint(stream, self.name)
|
|
|
|
else:
|
|
|
|
size += self.name.write(self)
|
|
|
|
if m:
|
|
|
|
size += write_real(stream, self.magnification)
|
|
|
|
if a:
|
|
|
|
size += write_real(stream, self.angle)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Text(Record):
|
|
|
|
"""
|
|
|
|
Text record (ID 19)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.string (AString or int or None, None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means reuse modal)
|
|
|
|
.y (int or None, None means reuse modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
string = None # type: AString or int or None
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
string: AString or str or int = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param string: Text content, or TextString reference number.
|
|
|
|
Default None (use modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
if isinstance(string, str):
|
|
|
|
self.string = AString(string)
|
|
|
|
else:
|
|
|
|
self.string = string
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'text_x', 'text_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'string', modals, 'text_string')
|
|
|
|
adjust_field(self, 'layer', modals, 'text_layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'text_datatype')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'text_x', 'text_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'string', modals, 'text_string')
|
|
|
|
dedup_field(self, 'layer', modals, 'text_layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'text_datatype')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Text':
|
|
|
|
if record_id != 19:
|
|
|
|
raise InvalidDataError('Invalid record id for Text: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
z0, c, n, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
if z0:
|
|
|
|
raise InvalidDataError('Malformed Text header')
|
|
|
|
|
|
|
|
optional = {}
|
|
|
|
string = read_refstring(stream, c, n)
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
|
|
|
|
record = Text(string, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
c = self.string is not None
|
|
|
|
n = c and isinstance(self.string, int)
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 19)
|
|
|
|
size += write_bool_byte(stream, (0, c, n, x, y, r, d, l))
|
|
|
|
if c:
|
|
|
|
if n:
|
|
|
|
size += write_uint(stream, self.string)
|
|
|
|
else:
|
|
|
|
size += self.string.write(self)
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Rectangle(Record):
|
|
|
|
"""
|
|
|
|
Rectangle record (ID 20)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.is_square (bool, True if this is a square.
|
|
|
|
If True, height must be None.)
|
|
|
|
.width (int or None, None means reuse modal)
|
|
|
|
.height (int or None, Must be None if .is_square is True.
|
|
|
|
If .is_square is False, None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means use modal)
|
|
|
|
.y (int or None, None means use modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
width = None # type: int or None
|
|
|
|
height = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
is_square = None # type: bool
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
is_square: bool = False,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
width: int = None,
|
|
|
|
height: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param is_square: True if this is a square. If True, height must
|
|
|
|
be None. Default False.
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param width: X-width. Default None (reuse modal).
|
|
|
|
:param height: Y-height. Default None (reuse modal, or use width if
|
|
|
|
square). Must be None if is_square is True.
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.is_square = is_square
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
if is_square and self.height is not None:
|
|
|
|
raise InvalidDataError('Rectangle is square and also has height')
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
|
|
if self.is_square:
|
|
|
|
adjust_field(self, 'width', modals, 'geometry_h')
|
|
|
|
else:
|
|
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
|
|
if self.is_square:
|
|
|
|
dedup_field(self, 'width', modals, 'geometry_h')
|
|
|
|
else:
|
|
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Rectangle':
|
|
|
|
if record_id != 20:
|
|
|
|
raise InvalidDataError('Invalid record id for Rectangle: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
is_square, w, h, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if w:
|
|
|
|
optional['width'] = read_uint(stream)
|
|
|
|
if h:
|
|
|
|
optional['height'] = read_uint(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Rectangle(is_square, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
s = self.is_square
|
|
|
|
w = self.width is not None
|
|
|
|
h = self.height is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 20)
|
|
|
|
size += write_bool_byte(stream, (s, w, h, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if w:
|
|
|
|
size += write_uint(stream, self.width)
|
|
|
|
if h:
|
|
|
|
size += write_uint(stream, self.height)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Polygon(Record):
|
|
|
|
"""
|
|
|
|
Polygon record (ID 21)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.point_list ([[x0, y0], [x1, y1], ...] or None,
|
|
|
|
list is an implicitly closed path,
|
|
|
|
vertices are [int, int],
|
|
|
|
None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means reuse modal)
|
|
|
|
.y (int or None, None means reuse modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
point_list = None # type: List[List[int]] or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
point_list: List[List[int]] = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param point_list: List of vertices [[x0, y0], [x1, y1], ...].
|
|
|
|
List forms an implicitly closed path
|
|
|
|
Default None (reuse modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
self.point_list = point_list
|
|
|
|
|
|
|
|
if point_list is not None:
|
|
|
|
if len(point_list) < 3:
|
|
|
|
raise InvalidDataError('Polygon with < 3 points')
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'point_list', modals, 'polygon_point_list')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'point_list', modals, 'polygon_point_list')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Polygon':
|
|
|
|
if record_id != 21:
|
|
|
|
raise InvalidDataError('Invalid record id for Polygon: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
z0, z1, p, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
if z0 or z1:
|
|
|
|
raise InvalidDataError('Invalid polygon header')
|
|
|
|
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if p:
|
|
|
|
optional['point_list'] = read_point_list(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Polygon(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase, fast: bool = False) -> int:
|
|
|
|
p = self.point_list is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 21)
|
|
|
|
size += write_bool_byte(stream, (0, 0, p, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if p:
|
|
|
|
size += write_point_list(stream, self.point_list, implicit_closed=True, fast=fast)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Path(Record):
|
|
|
|
"""
|
|
|
|
Polygon record (ID 22)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.point_list ([[x0, y0], [x1, y1], ...] or None,
|
|
|
|
vertices are [int, int],
|
|
|
|
None means reuse modal)
|
|
|
|
.half_width (int or None, None means reuse modal)
|
|
|
|
.extension_start (Tuple or None,
|
|
|
|
None means reuse modal,
|
|
|
|
Tuple is of the form
|
|
|
|
(PathExtensionScheme, int or None)
|
|
|
|
second value is None unless using
|
|
|
|
PathExtensionScheme.Arbitrary
|
|
|
|
Value determines extension past start point.
|
|
|
|
.extension_end Same form as extension_end. Value determines
|
|
|
|
extension past end point.
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means use modal)
|
|
|
|
.y (int or None, None means use modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
point_list = None # type: List[List[int]] or None
|
|
|
|
half_width = None # type: int or None
|
|
|
|
extension_start = None # type: pathextension_t or None
|
|
|
|
extension_end = None # type: pathextension_t or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
point_list: List[List[int]] = None,
|
|
|
|
half_width: int = None,
|
|
|
|
extension_start: pathextension_t = None,
|
|
|
|
extension_end: pathextension_t = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param point_list: List of vertices [[x0, y0], [x1, y1], ...].
|
|
|
|
Default None (reuse modal).
|
|
|
|
:param half_width: Half-width of the path. Default None (reuse modal).
|
|
|
|
:param extension_start: Specification for path extension at start of path.
|
|
|
|
None or Tuple: (PathExtensionScheme, int or None).
|
|
|
|
int is used only for PathExtensionScheme.Arbitrary.
|
|
|
|
Default None (reuse modal).
|
|
|
|
:param extension_end: Specification for path extension at end of path.
|
|
|
|
None or Tuple: (PathExtensionScheme, int or None).
|
|
|
|
int is used only for PathExtensionScheme.Arbitrary.
|
|
|
|
Default None (reuse modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
"""
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
self.point_list = point_list
|
|
|
|
self.half_width = half_width
|
|
|
|
self.extension_start = extension_start
|
|
|
|
self.extension_end = extension_end
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'point_list', modals, 'path_point_list')
|
|
|
|
adjust_field(self, 'half_width', modals, 'path_half_width')
|
|
|
|
adjust_field(self, 'extension_start', modals, 'path_extension_start')
|
|
|
|
adjust_field(self, 'extension_end', modals, 'path_extension_end')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'point_list', modals, 'path_point_list')
|
|
|
|
dedup_field(self, 'half_width', modals, 'path_half_width')
|
|
|
|
dedup_field(self, 'extension_start', modals, 'path_extension_start')
|
|
|
|
dedup_field(self, 'extension_end', modals, 'path_extension_end')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Path':
|
|
|
|
if record_id != 22:
|
|
|
|
raise InvalidDataError('Invalid record id for Path: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
e, w, p, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if w:
|
|
|
|
optional['half_width'] = read_uint(stream)
|
|
|
|
if e:
|
|
|
|
scheme = read_uint(stream)
|
|
|
|
scheme_end = scheme & 0b11
|
|
|
|
scheme_start = (scheme >> 2) & 0b11
|
|
|
|
|
|
|
|
def get_pathext(ext_scheme: int) -> pathextension_t:
|
|
|
|
if ext_scheme == 0:
|
|
|
|
return None
|
|
|
|
elif ext_scheme == 1:
|
|
|
|
return PathExtensionScheme.Flush, None
|
|
|
|
elif ext_scheme == 2:
|
|
|
|
return PathExtensionScheme.HalfWidth, None
|
|
|
|
elif ext_scheme == 3:
|
|
|
|
return PathExtensionScheme.Arbitrary, read_sint(stream)
|
|
|
|
|
|
|
|
optional['extension_start'] = get_pathext(scheme_start)
|
|
|
|
optional['extension_end'] = get_pathext(scheme_end)
|
|
|
|
if p:
|
|
|
|
optional['point_list'] = read_point_list(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Path(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase, fast: bool = False) -> int:
|
|
|
|
e = self.extension_start is not None or self.extension_end is not None
|
|
|
|
w = self.half_width is not None
|
|
|
|
p = self.point_list is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 21)
|
|
|
|
size += write_bool_byte(stream, (e, w, p, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if w:
|
|
|
|
size += write_uint(stream, self.half_width)
|
|
|
|
if e:
|
|
|
|
scheme = 0
|
|
|
|
if self.extension_start is not None:
|
|
|
|
scheme += self.extension_start[0].value << 2
|
|
|
|
if self.extension_end is not None:
|
|
|
|
scheme += self.extension_end[0].value
|
|
|
|
size += write_uint(stream, scheme)
|
|
|
|
if scheme & 0b1100 == 0b1100:
|
|
|
|
size += write_sint(stream, self.extension_start[1])
|
|
|
|
if scheme & 0b0011 == 0b0011:
|
|
|
|
size += write_sint(stream, self.extension_end[1])
|
|
|
|
if p:
|
|
|
|
size += write_point_list(stream, self.point_list, implicit_closed=False, fast=fast)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Trapezoid(Record):
|
|
|
|
"""
|
|
|
|
Trapezoid record (ID 23, 24, 25)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.delta_a (int or None,
|
|
|
|
If horizontal, signed x-distance from top left
|
|
|
|
vertex to bottom left vertex. If vertical, signed
|
|
|
|
y-distance from bottom left vertex to bottom right
|
|
|
|
vertex.
|
|
|
|
None means reuse modal.)
|
|
|
|
.delta_b (int or None,
|
|
|
|
If horizontal, signed x-distance from bottom right
|
|
|
|
vertex to top right vertex. If vertical, signed
|
|
|
|
y-distance from top right vertex to top left vertex.
|
|
|
|
None means reuse modal.)
|
|
|
|
.is_vertical (bool, True if the left and right sides are aligned to
|
|
|
|
the y-axis. If the trapezoid is a rectangle, either
|
|
|
|
True or False can be used.)
|
|
|
|
.width (int or None, Bounding box x-width, None means reuse modal)
|
|
|
|
.height (int or None, Bounding box y-height, None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means se modal)
|
|
|
|
.y (int or None, None means se modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
width = None # type: int or None
|
|
|
|
height = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
delta_a = None # type: int
|
|
|
|
delta_b = None # type: int
|
|
|
|
is_vertical = None # type: bool
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
is_vertical: bool,
|
|
|
|
delta_a: int = 0,
|
|
|
|
delta_b: int = 0,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
width: int = None,
|
|
|
|
height: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param is_vertical: True if both the left and right sides are aligned
|
|
|
|
to the y-axis. If the trapezoid is a rectangle, either value
|
|
|
|
is permitted.
|
|
|
|
:param delta_a: If horizontal, signed x-distance from top-left vertex
|
|
|
|
to bottom-left vertex. If vertical, signed y-distance from bottom-
|
|
|
|
left vertex to bottom-right vertex. Default None (reuse modal).
|
|
|
|
:param delta_b: If horizontal, signed x-distance from bottom-right vertex
|
|
|
|
to top right vertex. If vertical, signed y-distance from top-right
|
|
|
|
vertex to top-left vertex. Default None (reuse modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param width: X-width of bounding box. Default None (reuse modal).
|
|
|
|
:param height: Y-height of bounding box. Default None (reuse modal)
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
:raises: InvalidDataError if dimensions are impossible.
|
|
|
|
"""
|
|
|
|
self.is_vertical = is_vertical
|
|
|
|
self.delta_a = delta_a
|
|
|
|
self.delta_b = delta_b
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
|
|
|
|
if self.is_vertical:
|
|
|
|
if height is not None and delta_b - delta_a > height:
|
|
|
|
raise InvalidDataError('Trapezoid: h < delta_b - delta_a'
|
|
|
|
' ({} < {} - {})'.format(height, delta_b, delta_a))
|
|
|
|
else:
|
|
|
|
if width is not None and delta_b - delta_a > width:
|
|
|
|
raise InvalidDataError('Trapezoid: w < delta_b - delta_a'
|
|
|
|
' ({} < {} - {})'.format(width, delta_b, delta_a))
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Trapezoid':
|
|
|
|
if record_id not in (23, 24, 25):
|
|
|
|
raise InvalidDataError('Invalid record id for Trapezoid: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
is_vertical, w, h, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if w:
|
|
|
|
optional['width'] = read_uint(stream)
|
|
|
|
if h:
|
|
|
|
optional['height'] = read_uint(stream)
|
|
|
|
if record_id != 25:
|
|
|
|
optional['delta_a'] = read_sint(stream)
|
|
|
|
if record_id != 24:
|
|
|
|
optional['delta_b'] = read_sint(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Trapezoid(is_vertical, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
v = self.is_vertical
|
|
|
|
w = self.width is not None
|
|
|
|
h = self.height is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
if self.delta_b == 0:
|
|
|
|
record_id = 24
|
|
|
|
elif self.delta_a == 0:
|
|
|
|
record_id = 25
|
|
|
|
else:
|
|
|
|
record_id = 23
|
|
|
|
size = write_uint(stream, record_id)
|
|
|
|
size += write_bool_byte(stream, (v, w, h, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if w:
|
|
|
|
size += write_uint(stream, self.width)
|
|
|
|
if h:
|
|
|
|
size += write_uint(stream, self.height)
|
|
|
|
if record_id != 25:
|
|
|
|
size += write_sint(stream, self.delta_a)
|
|
|
|
if record_id != 24:
|
|
|
|
size += write_sint(stream, self.delta_b)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: CTrapezoid type descriptions
|
|
|
|
class CTrapezoid(Record):
|
|
|
|
"""
|
|
|
|
CTrapezoid record (ID 26)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.ctrapezoid_type (int or None, see OASIS spec for details, None means reuse modal)
|
|
|
|
.width (int or None, Bounding box x-width, None means reuse modal)
|
|
|
|
.height (int or None, Bounding box y-height, None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means se modal)
|
|
|
|
.y (int or None, None means se modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
ctrapezoid_type = None # type: int or None
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
width = None # type: int or None
|
|
|
|
height = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
ctrapezoid_type: int = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
width: int = None,
|
|
|
|
height: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param ctrapezoid_type: CTrapezoid type; see OASIS format
|
|
|
|
documentation. Default None (reuse modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param width: X-width of bounding box. Default None (reuse modal).
|
|
|
|
:param height: Y-height of bounding box. Default None (reuse modal)
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
:raises: InvalidDataError if dimensions are invalid.
|
|
|
|
"""
|
|
|
|
self.ctrapezoid_type = ctrapezoid_type
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
|
|
|
|
if ctrapezoid_type in (20, 21) and width is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious width entry: '
|
|
|
|
'{}'.format(width))
|
|
|
|
if ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25) and height is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious height entry: '
|
|
|
|
'{}'.format(height))
|
|
|
|
if ctrapezoid_type in range(0, 4) and width < height:
|
|
|
|
raise InvalidDataError('CTrapezoid has width < height'
|
|
|
|
' ({} < {})'.format(width, height))
|
|
|
|
if ctrapezoid_type in range(4, 8) and width < 2 * height:
|
|
|
|
raise InvalidDataError('CTrapezoid has width < 2*height'
|
|
|
|
' ({} < 2 * {})'.format(width, height))
|
|
|
|
if ctrapezoid_type in range(8, 12) and width > height:
|
|
|
|
raise InvalidDataError('CTrapezoid has width > height'
|
|
|
|
' ({} > {})'.format(width, height))
|
|
|
|
if ctrapezoid_type in range(12, 16) and 2 * width > height:
|
|
|
|
raise InvalidDataError('CTrapezoid has 2*width > height'
|
|
|
|
' ({} > 2 * {})'.format(width, height))
|
|
|
|
if ctrapezoid_type is not None and ctrapezoid_type not in range(0, 26):
|
|
|
|
raise InvalidDataError('CTrapezoid has invalid type: '
|
|
|
|
'{}'.format(ctrapezoid_type))
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type')
|
|
|
|
|
|
|
|
if self.ctrapezoid_type in (20, 21):
|
|
|
|
if self.width is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious width entry: '
|
|
|
|
'{}'.format(self.width))
|
|
|
|
else:
|
|
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
|
|
|
|
|
|
if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25):
|
|
|
|
if self.height is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious height entry: '
|
|
|
|
'{}'.format(self.height))
|
|
|
|
else:
|
|
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
dedup_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type')
|
|
|
|
|
|
|
|
if self.ctrapezoid_type in (20, 21):
|
|
|
|
if self.width is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious width entry: '
|
|
|
|
'{}'.format(self.width))
|
|
|
|
else:
|
|
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
|
|
|
|
|
|
if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25):
|
|
|
|
if self.height is not None:
|
|
|
|
raise InvalidDataError('CTrapezoid has spurious height entry: '
|
|
|
|
'{}'.format(self.height))
|
|
|
|
else:
|
|
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'CTrapezoid':
|
|
|
|
if record_id != 26:
|
|
|
|
raise InvalidDataError('Invalid record id for CTrapezoid: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
t, w, h, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if t:
|
|
|
|
optional['ctrapezoid_type'] = read_uint(stream)
|
|
|
|
if w:
|
|
|
|
optional['width'] = read_uint(stream)
|
|
|
|
if h:
|
|
|
|
optional['height'] = read_uint(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = CTrapezoid(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
t = self.ctrapezoid_type is not None
|
|
|
|
w = self.width is not None
|
|
|
|
h = self.height is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 26)
|
|
|
|
size += write_bool_byte(stream, (t, w, h, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if t:
|
|
|
|
size += write_uint(stream, self.ctrapezoid_type)
|
|
|
|
if w:
|
|
|
|
size += write_uint(stream, self.width)
|
|
|
|
if h:
|
|
|
|
size += write_uint(stream, self.height)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
class Circle(Record):
|
|
|
|
"""
|
|
|
|
Circle record (ID 27)
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
.radius (int or None, None means reuse modal)
|
|
|
|
.layer (int or None, None means reuse modal)
|
|
|
|
.datatype (int or None, None means reuse modal)
|
|
|
|
.x (int or None, None means se modal)
|
|
|
|
.y (int or None, None means se modal)
|
|
|
|
.repetition (reptetition or None)
|
|
|
|
"""
|
|
|
|
layer = None # type: int or None
|
|
|
|
datatype = None # type: int or None
|
|
|
|
x = None # type: int or None
|
|
|
|
y = None # type: int or None
|
|
|
|
repetition = None # type: repetition_t or None
|
|
|
|
radius = None # type: int or None
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
radius: int = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
|
|
|
:param radius: Radius. Default None (reuse modal).
|
|
|
|
:param layer: Layer number. Default None (reuse modal).
|
|
|
|
:param datatype: Datatype number. Default None (reuse modal).
|
|
|
|
:param x: X-offset. Default None (use modal).
|
|
|
|
:param y: Y-offset. Default None (use modal).
|
|
|
|
:param repetition: Repetition. Default None (no repetition).
|
|
|
|
:raises: InvalidDataError if dimensions are invalid.
|
|
|
|
"""
|
|
|
|
self.radius = radius
|
|
|
|
self.layer = layer
|
|
|
|
self.datatype = datatype
|
|
|
|
self.x = x
|
|
|
|
self.y = y
|
|
|
|
self.repetition = repetition
|
|
|
|
|
|
|
|
def merge_with_modals(self, modals: Modals):
|
|
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
adjust_repetition(self, modals)
|
|
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
adjust_field(self, 'radius', modals, 'circle_radius')
|
|
|
|
|
|
|
|
def deduplicate_with_modals(self, modals: Modals):
|
|
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
|
|
dedup_repetition(self, modals)
|
|
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
dedup_field(self, 'radius', modals, 'circle_radius')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read(stream: io.BufferedIOBase, record_id: int) -> 'Circle':
|
|
|
|
if record_id == 27:
|
|
|
|
raise InvalidDataError('Invalid record id for Circle: '
|
|
|
|
'{}'.format(record_id))
|
|
|
|
|
|
|
|
z0, z1, has_radius, x, y, r, d, l = read_bool_byte(stream)
|
|
|
|
if z0 or z1:
|
|
|
|
raise InvalidDataError('Malformed circle header')
|
|
|
|
|
|
|
|
optional = {}
|
|
|
|
if l:
|
|
|
|
optional['layer'] = read_uint(stream)
|
|
|
|
if d:
|
|
|
|
optional['datatype'] = read_uint(stream)
|
|
|
|
if has_radius:
|
|
|
|
optional['radius'] = read_uint(stream)
|
|
|
|
if x:
|
|
|
|
optional['x'] = read_sint(stream)
|
|
|
|
if y:
|
|
|
|
optional['y'] = read_sint(stream)
|
|
|
|
if r:
|
|
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Circle(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
s = self.radius is not None
|
|
|
|
x = self.x is not None
|
|
|
|
y = self.y is not None
|
|
|
|
r = self.repetition is not None
|
|
|
|
d = self.datatype is not None
|
|
|
|
l = self.layer is not None
|
|
|
|
|
|
|
|
size = write_uint(stream, 27)
|
|
|
|
size += write_bool_byte(stream, (0, 0, s, x, y, r, d, l))
|
|
|
|
if l:
|
|
|
|
size += write_uint(stream, self.layer)
|
|
|
|
if d:
|
|
|
|
size += write_uint(stream, self.datatype)
|
|
|
|
if s:
|
|
|
|
size += write_uint(stream, self.radius)
|
|
|
|
if x:
|
|
|
|
size += write_sint(stream, self.x)
|
|
|
|
if y:
|
|
|
|
size += write_sint(stream, self.y)
|
|
|
|
if r:
|
|
|
|
size += self.repetition.write(stream)
|
|
|
|
return size
|
|
|
|
|
|
|
|
|
|
|
|
def adjust_repetition(record: Record, modals: Modals):
|
|
|
|
"""
|
|
|
|
Merge the record's repetition entry with the one in the modals
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:raises: InvalidDataError if a ReuseRepetition can't be filled
|
|
|
|
from the modals.
|
|
|
|
"""
|
|
|
|
if record.repetition is not None:
|
|
|
|
if isinstance(record.repetition, ReuseRepetition):
|
|
|
|
if modals.repetition is None:
|
|
|
|
raise InvalidDataError('Unfillable repetition')
|
|
|
|
else:
|
|
|
|
record.repetition = copy.copy(modals.repetition)
|
|
|
|
else:
|
|
|
|
modals.repetition = copy.copy(record.repetition)
|
|
|
|
|
|
|
|
|
|
|
|
def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str):
|
|
|
|
"""
|
|
|
|
Merge record.r_field with modals.m_field
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param r_field: Attr of record to access.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:param m_field: Attr of modals to access.
|
|
|
|
:raises: InvalidDataError if a both fields are None
|
|
|
|
"""
|
|
|
|
r = getattr(record, r_field)
|
|
|
|
if r is not None:
|
|
|
|
setattr(modals, m_field, r)
|
|
|
|
else:
|
|
|
|
m = getattr(modals, m_field)
|
|
|
|
if m is not None:
|
|
|
|
setattr(record, r_field, copy.copy(m))
|
|
|
|
else:
|
|
|
|
raise InvalidDataError('Unfillable field: {}'.format(m_field))
|
|
|
|
|
|
|
|
|
|
|
|
def adjust_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str):
|
|
|
|
"""
|
|
|
|
Merge record.x and record.y with modals.mx_field and modals.my_field,
|
|
|
|
taking into account the value of modals.xy_relative.
|
|
|
|
|
|
|
|
If modals.xy_relative is True and the record has non-None coordinates,
|
|
|
|
the modal values are added to the record's coordinates. If modals.xy_relative
|
|
|
|
is False, the coordinates are treated the same way as other fields.
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:param mx_field: Attr of modals corresponding to record.x
|
|
|
|
:param my_field: Attr of modals corresponding to record.y
|
|
|
|
:raises: InvalidDataError if a both fields are None
|
|
|
|
"""
|
|
|
|
if record.x is not None:
|
|
|
|
if modals.xy_relative:
|
|
|
|
record.x += getattr(modals, mx_field)
|
|
|
|
else:
|
|
|
|
setattr(modals, mx_field, record.x)
|
|
|
|
else:
|
|
|
|
record.x = getattr(modals, mx_field)
|
|
|
|
|
|
|
|
if record.y is not None:
|
|
|
|
if modals.xy_relative:
|
|
|
|
record.y += getattr(modals, my_field)
|
|
|
|
else:
|
|
|
|
setattr(modals, my_field, record.y)
|
|
|
|
else:
|
|
|
|
record.y = getattr(modals, my_field)
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: Clarify the docs on the dedup_* functions
|
|
|
|
def dedup_repetition(record: Record, modals: Modals):
|
|
|
|
"""
|
|
|
|
Deduplicate the record's repetition entry with the one in the modals.
|
|
|
|
Update the one in the modals if they are different.
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:raises: InvalidDataError if a ReuseRepetition can't be filled
|
|
|
|
from the modals.
|
|
|
|
"""
|
|
|
|
if record.repetition is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
if isinstance(record.repetition, ReuseRepetition):
|
|
|
|
if modals.repetition is None:
|
|
|
|
raise InvalidDataError('Unfillable repetition')
|
|
|
|
return
|
|
|
|
|
|
|
|
if record.repetition == modals.repetition:
|
|
|
|
record.repetition = ReuseRepetition()
|
|
|
|
else:
|
|
|
|
modals.repetition = record.repetition
|
|
|
|
|
|
|
|
|
|
|
|
def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str):
|
|
|
|
"""
|
|
|
|
Deduplicate record.r_field using modals.m_field
|
|
|
|
Update the modals.m_field if they are different.
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param r_field: Attr of record to access.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:param m_field: Attr of modals to access.
|
|
|
|
:raises: InvalidDataError if a both fields are None
|
|
|
|
"""
|
|
|
|
r = getattr(record, r_field)
|
|
|
|
m = getattr(modals, m_field)
|
|
|
|
if r is not None:
|
|
|
|
if m is not None and m == r:
|
|
|
|
setattr(record, r_field, None)
|
|
|
|
else:
|
|
|
|
setattr(modals, m_field, r)
|
|
|
|
elif m is None:
|
|
|
|
raise InvalidDataError('Unfillable field')
|
|
|
|
|
|
|
|
|
|
|
|
def dedup_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str):
|
|
|
|
"""
|
|
|
|
Deduplicate record.x and record.y using modals.mx_field and modals.my_field,
|
|
|
|
taking into account the value of modals.xy_relative.
|
|
|
|
|
|
|
|
If modals.xy_relative is True and the record has non-None coordinates,
|
|
|
|
the modal values are subtracted from the record's coordinates. If modals.xy_relative
|
|
|
|
is False, the coordinates are treated the same way as other fields.
|
|
|
|
|
|
|
|
:param record: Record to read or modify.
|
|
|
|
:param modals: Modals to read or modify.
|
|
|
|
:param mx_field: Attr of modals corresponding to record.x
|
|
|
|
:param my_field: Attr of modals corresponding to record.y
|
|
|
|
:raises: InvalidDataError if a both fields are None
|
|
|
|
"""
|
|
|
|
if record.x is not None:
|
|
|
|
mx = getattr(modals, mx_field)
|
|
|
|
if modals.xy_relative:
|
|
|
|
record.x -= mx
|
|
|
|
else:
|
|
|
|
if record.x == mx:
|
|
|
|
record.x = None
|
|
|
|
else:
|
|
|
|
setattr(modals, mx_field, record.x)
|
|
|
|
|
|
|
|
if record.y is not None:
|
|
|
|
my = getattr(modals, my_field)
|
|
|
|
if modals.xy_relative:
|
|
|
|
record.y -= my
|
|
|
|
else:
|
|
|
|
if record.y == my:
|
|
|
|
record.y = None
|
|
|
|
else:
|
|
|
|
setattr(modals, my_field, record.y)
|
|
|
|
|