2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
2020-04-18 15:38:52 -07:00
|
|
|
from typing import List, Dict, Tuple, Union, Optional, Sequence, Any
|
2017-09-18 03:01:48 -07:00
|
|
|
import copy
|
|
|
|
import math
|
|
|
|
import zlib
|
|
|
|
import io
|
2018-07-21 13:43:24 -07:00
|
|
|
import logging
|
|
|
|
import pprint
|
2019-09-08 02:25:04 -07:00
|
|
|
from warnings import warn
|
2017-09-18 03:01:48 -07:00
|
|
|
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, \
|
2020-04-18 03:00:36 -07:00
|
|
|
InvalidDataError, PathExtensionScheme, _USE_NUMPY
|
|
|
|
|
|
|
|
if _USE_NUMPY:
|
|
|
|
import numpy
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2018-07-21 13:43:24 -07:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-09-18 03:01:48 -07:00
|
|
|
'''
|
|
|
|
Type definitions
|
|
|
|
'''
|
2020-04-18 03:03:35 -07:00
|
|
|
geometry_t = Union['Text', 'Rectangle', 'Polygon', 'Path', 'Trapezoid',
|
|
|
|
'CTrapezoid', 'Circle', 'XElement', 'XGeometry']
|
|
|
|
pathextension_t = Tuple['PathExtensionScheme', Optional[int]]
|
|
|
|
point_list_t = Sequence[Sequence[int]]
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
|
|
|
|
class Modals:
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Modal variables, used to store data about previously-written or
|
2017-09-18 03:01:48 -07:00
|
|
|
-read records.
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
placement_x: int = 0
|
|
|
|
placement_y: int = 0
|
|
|
|
placement_cell: Optional[NString] = None
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
text_layer: Optional[int] = None
|
|
|
|
text_datatype: Optional[int] = None
|
|
|
|
text_x: int = 0
|
|
|
|
text_y: int = 0
|
|
|
|
text_string: Union[AString, int, None] = None
|
|
|
|
geometry_x: int = 0
|
|
|
|
geometry_y: int = 0
|
|
|
|
xy_relative: bool = False
|
|
|
|
geometry_w: Optional[int] = None
|
|
|
|
geometry_h: Optional[int] = None
|
|
|
|
polygon_point_list: Optional[point_list_t] = None
|
|
|
|
path_half_width: Optional[int] = None
|
|
|
|
path_point_list: Optional[point_list_t] = None
|
|
|
|
path_extension_start: Optional[pathextension_t] = None
|
|
|
|
path_extension_end: Optional[pathextension_t] = None
|
|
|
|
ctrapezoid_type: Optional[int] = None
|
|
|
|
circle_radius: Optional[int] = None
|
|
|
|
property_value_list: Optional[Sequence[property_value_t]] = None
|
|
|
|
property_name: Union[int, NString, None] = None
|
|
|
|
property_is_standard: Optional[bool] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.reset()
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
"""
|
|
|
|
Resets all modal variables to their default values.
|
|
|
|
Default values are:
|
2020-04-18 03:03:35 -07:00
|
|
|
`0` for placement_{x,y}, text_{x,y}, geometry_{x,y}
|
|
|
|
`False` for xy_relative
|
|
|
|
Undefined (`None`) for all others
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
2020-04-18 03:03:35 -07:00
|
|
|
self.path_half_width = None
|
2017-09-18 03:01:48 -07:00
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
modals: Modal variables to merge with.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
modals: Modal variables to deduplicate with.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
stream: Stream to read from.
|
|
|
|
record_id: Record id of the record to read. The
|
2017-09-18 03:01:48 -07:00
|
|
|
record id is often used to specify which variant
|
|
|
|
of the record is stored.
|
2020-04-18 03:03:35 -07:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
The record that was read.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if the record is malformed.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
stream: Stream to write to.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Number of bytes written.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if the record contains invalid data.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int:
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Run `.deduplicate_with_modals()` and then `.write()` to the stream.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
stream: Stream to write to.
|
|
|
|
modals: Modal variables to merge with.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Number of bytes written.
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Raises:
|
|
|
|
InvalidDataError: if the record contains invalid data.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
# 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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Returns:
|
|
|
|
A deep copy of this record.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
return copy.deepcopy(self)
|
|
|
|
|
2018-07-21 13:43:24 -07:00
|
|
|
def __repr__(self) -> str:
|
|
|
|
return '{}: {}'.format(self.__class__, pprint.pformat(self.__dict__))
|
|
|
|
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def read_refname(stream: io.BufferedIOBase,
|
|
|
|
is_present: bool,
|
|
|
|
is_reference: bool
|
2020-04-18 03:03:35 -07:00
|
|
|
) -> Union[None, int, NString]:
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
Helper function for reading a possibly-absent, possibly-referenced NString.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
stream: Stream to read from.
|
|
|
|
is_present: If `False`, read nothing and return `None`
|
|
|
|
is_reference: If `True`, read a uint (reference id),
|
|
|
|
otherwise read an `NString`.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
`None`, reference id, or `NString`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
2020-04-18 03:03:35 -07:00
|
|
|
) -> Union[None, int, AString]:
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Helper function for reading a possibly-absent, possibly-referenced `AString`.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
stream: Stream to read from.
|
|
|
|
is_present: If `False`, read nothing and return `None`
|
|
|
|
is_reference: If `True`, read a uint (reference id),
|
|
|
|
otherwise read an `AString`.
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Returns:
|
|
|
|
`None`, reference id, or `AString`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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()
|
2018-07-21 13:46:22 -07:00
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
2017-09-18 03:01:48 -07:00
|
|
|
return record
|
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
return write_uint(stream, 0)
|
|
|
|
|
|
|
|
|
|
|
|
class XYMode(Record):
|
|
|
|
"""
|
|
|
|
XYMode record (ID 15, 16)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
relative (bool): default `False`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
relative: bool = False
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def absolute(self) -> bool:
|
2020-04-18 15:39:27 -07:00
|
|
|
return not self.relative
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
@absolute.setter
|
|
|
|
def absolute(self, b: bool):
|
|
|
|
self.relative = not b
|
|
|
|
|
|
|
|
def __init__(self, relative: bool):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
relative: `True` if the mode is 'relative', `False` if 'absolute'.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2018-07-21 13:44:12 -07:00
|
|
|
self.relative = relative
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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')
|
2018-07-21 13:46:22 -07:00
|
|
|
record = XYMode(record_id == 16)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def write(self, stream: io.BufferedIOBase) -> int:
|
|
|
|
return write_uint(stream, 15 + self.relative)
|
|
|
|
|
|
|
|
|
|
|
|
class Start(Record):
|
|
|
|
"""
|
|
|
|
Start Record (ID 1)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
version (AString): "1.0"
|
|
|
|
unit (real_t): positive real number, grid steps per micron
|
2020-04-18 15:38:52 -07:00
|
|
|
offset_table (Optional[OffsetTable]): If `None` then table must be
|
2020-04-18 03:03:35 -07:00
|
|
|
placed in the `End` record)
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
version: AString
|
|
|
|
unit: real_t
|
2020-04-18 15:38:52 -07:00
|
|
|
offset_table: Optional[OffsetTable] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
unit: real_t,
|
2020-04-18 03:03:35 -07:00
|
|
|
version: Union[AString, str] = None,
|
2020-04-18 15:38:52 -07:00
|
|
|
offset_table: Optional[OffsetTable] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args
|
|
|
|
unit: Grid steps per micron (positive real number)
|
|
|
|
version: Version string, default "1.0"
|
|
|
|
offset_table: `OffsetTable` for the file, or `None` to place
|
|
|
|
it in the `End` record instead.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
2020-04-18 15:38:52 -07:00
|
|
|
offset_table: Optional[OffsetTable]
|
2017-09-18 03:01:48 -07:00
|
|
|
if has_offset_table:
|
|
|
|
offset_table = OffsetTable.read(stream)
|
|
|
|
else:
|
|
|
|
offset_table = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Start(unit, version, offset_table)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
offset_table (Optional[OffsetTable]): `None` if offset table was
|
|
|
|
written into the `Start` record instead
|
|
|
|
validation (Validation): object containing checksum
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
offset_table: Optional[OffsetTable] = None
|
|
|
|
validation: Validation
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
validation: Validation,
|
2020-04-18 03:03:35 -07:00
|
|
|
offset_table: Optional[OffsetTable] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
validation: `Validation` object for this file.
|
|
|
|
offset_table: `OffsetTable`, or `None` if the `Start` record
|
|
|
|
contained an `OffsetTable`. Default `None`.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
offset_table: Optional[OffsetTable] = OffsetTable.read(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
offset_table = None
|
|
|
|
_padding_string = read_bstring(stream)
|
|
|
|
validation = Validation.read(stream)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = End(validation, offset_table)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
compression_type (int): `0` for zlib
|
|
|
|
decompressed_byte_count (int): size after decompressing
|
|
|
|
compressed_bytes (bytes): compressed data
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
compression_type: int
|
|
|
|
decompressed_byte_count: int
|
|
|
|
compressed_bytes: bytes
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
compression_type: int,
|
|
|
|
decompressed_byte_count: int,
|
|
|
|
compressed_bytes: bytes):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
compression_type: `0` (zlib)
|
|
|
|
decompressed_byte_count: Number of bytes in the decompressed data.
|
|
|
|
compressed_bytes: The compressed data.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2018-07-21 14:04:59 -07:00
|
|
|
record = CBlock(compression_type, decompressed_count, compressed_bytes)
|
|
|
|
logger.debug('CBlock ending at 0x{:x} was read successfully'.format(stream.tell()))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
decompressed_bytes: Uncompressed data (one or more non-CBlock records)
|
|
|
|
compression_type: Compression type (0: zlib). Default `0`
|
|
|
|
compression_args: Passed as kwargs to `zlib.compressobj()`. Default `{}`.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
CBlock object constructed from the data.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if invalid `compression_type`.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
2018-07-21 13:46:48 -07:00
|
|
|
def decompress(self, decompression_args: Dict = None) -> bytes:
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
Decompress the contents of this CBlock.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
decompression_args: Passed as kwargs to `zlib.decompressobj()`.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Decompressed `bytes` object.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if data is malformed or compression type is
|
2017-09-18 03:01:48 -07:00
|
|
|
unknonwn.
|
|
|
|
"""
|
2018-07-21 13:46:48 -07:00
|
|
|
if decompression_args is None:
|
|
|
|
decompression_args = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
nstring (NString): name
|
|
|
|
reference_number (Optional[int]): `None` results in implicit assignment
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: NString
|
|
|
|
reference_number: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: Union[NString, str],
|
2017-09-18 03:01:48 -07:00
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
nstring: The contained string.
|
|
|
|
reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
reference_number: Optional[int] = read_uint(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
reference_number = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = CellName(nstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
nstring (NString): name
|
|
|
|
reference_number (Optional[int]): `None` results in implicit assignment
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: NString
|
|
|
|
reference_number: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: Union[NString, str],
|
2017-09-18 03:01:48 -07:00
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
nstring: The contained string.
|
|
|
|
reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
reference_number: Optional[int] = read_uint(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
reference_number = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = PropName(nstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
astring (AString): string data
|
|
|
|
reference_number (Optional[int]): `None` results in implicit assignment
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
astring: AString
|
|
|
|
reference_number: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
string: Union[AString, str],
|
2017-09-18 03:01:48 -07:00
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
string: The contained string.
|
|
|
|
reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
reference_number: Optional[int] = read_uint(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
reference_number = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = TextString(astring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
astring (AString): string data
|
|
|
|
reference_number (Optional[int]): `None` results in implicit assignment
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
astring: AString
|
|
|
|
reference_number: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
string: Union[AString, str],
|
2017-09-18 03:01:48 -07:00
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
string: The contained string.
|
|
|
|
reference_number: Reference id number for the string.
|
|
|
|
Default is to use an implicitly-assigned number.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2018-07-21 13:47:18 -07:00
|
|
|
if isinstance(string, AString):
|
|
|
|
self.astring = string
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
2018-07-21 13:47:18 -07:00
|
|
|
self.astring = AString(string)
|
2017-09-18 03:01:48 -07:00
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
reference_number: Optional[int] = read_uint(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
reference_number = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = PropString(astring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
nstring (NString): name
|
|
|
|
layer_interval (Tuple[Optional[int], Optional[int]]): bounds on the interval
|
|
|
|
type_interval (Tuple[Optional[int], Optional[int]]): bounds on the interval
|
|
|
|
is_textlayer (bool): Is this a text layer?
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: NString
|
|
|
|
layer_interval: Tuple
|
|
|
|
type_interval: Tuple
|
|
|
|
is_textlayer: bool
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
nstring: Union[NString, str],
|
2017-09-18 03:01:48 -07:00
|
|
|
layer_interval: Tuple,
|
|
|
|
type_interval: Tuple,
|
|
|
|
is_textlayer: bool):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
nstring: The layer name.
|
|
|
|
layer_interval: Tuple (int or None, int or None) giving bounds
|
|
|
|
(or lack of thereof) on the layer number.
|
|
|
|
type_interval: Tuple (int or None, int or None) giving bounds
|
|
|
|
(or lack of thereof) on the type number.
|
|
|
|
is_textlayer: `True` if the layer is a text layer.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2018-07-21 13:50:55 -07:00
|
|
|
nstring = NString.read(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
layer_interval = read_interval(stream)
|
|
|
|
type_interval = read_interval(stream)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = LayerName(nstring, layer_interval, type_interval, is_textlayer)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
name (Union[NString, int, None]): `int` is an explicit reference,
|
|
|
|
`None` is a flag to use Modal)
|
|
|
|
values (Optional[List[property_value_t]]): List of property values.
|
|
|
|
is_standard (bool): Whether this is a standard property.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
name: Union[NString, int, None] = None
|
|
|
|
values: Optional[List[property_value_t]] = None
|
|
|
|
is_standard: Optional[bool] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
name: Union[NString, str, int] = None,
|
|
|
|
values: Optional[List[property_value_t]] = None,
|
|
|
|
is_standard: Optional[bool] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
name: Property name, reference number, or `None` (i.e. use modal)
|
|
|
|
Default `None.
|
|
|
|
values: List of property values, or `None` (i.e. use modal)
|
|
|
|
Default `None`.
|
|
|
|
is_standard: `True` if this is a standard property. `None` to use modal.
|
|
|
|
Default `None`.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Property()
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2020-04-18 15:38:52 -07:00
|
|
|
values: Optional[List[property_value_t]]
|
2017-09-18 03:01:48 -07:00
|
|
|
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')
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Property(name, values, s)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
attribute (int): Attribute number
|
|
|
|
bstring (bytes): XName data
|
|
|
|
reference_number (Optional[int]): None means to use implicit numbering
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
attribute: int
|
|
|
|
bstring: bytes
|
|
|
|
reference_number: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
attribute: int,
|
|
|
|
bstring: bytes,
|
|
|
|
reference_number: int = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
attribute: Attribute number.
|
|
|
|
bstring: Binary XName data.
|
|
|
|
reference_number: Reference number for this `XName`.
|
|
|
|
Default `None` (implicit).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2020-04-18 15:38:52 -07:00
|
|
|
reference_number: Optional[int] = read_uint(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
|
|
|
reference_number = None
|
2018-07-21 13:46:22 -07:00
|
|
|
record = XName(attribute, bstring, reference_number)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
attribute (int): Attribute number.
|
|
|
|
bstring (bytes): XElement data.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
attribute: int
|
|
|
|
bstring: bytes
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self, attribute: int, bstring: bytes):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
attribute: Attribute number.
|
|
|
|
bstring: Binary data for this XElement.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = XElement(attribute, bstring)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
attribute (int): Attribute number.
|
|
|
|
bstring (bytes): XGeometry data.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): None means reuse modal
|
|
|
|
y (Optional[int]): None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
attribute: int
|
|
|
|
bstring: bytes
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
attribute: int,
|
|
|
|
bstring: bytes,
|
2020-04-18 15:38:52 -07:00
|
|
|
layer: Optional[int] = None,
|
|
|
|
datatype: Optional[int] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
attribute: Attribute number for this XGeometry.
|
|
|
|
bstring: Binary data for this XGeometry.
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2018-07-21 13:46:22 -07:00
|
|
|
record = XGeometry(attribute, bstring, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
name (Union[int, NString]): int specifies "CellName reference" number
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
name: Union[int, NString]
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
def __init__(self, name: Union[int, NString]):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
name: `NString`, or an int specifying a `CellName` reference number.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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':
|
2020-04-18 15:38:52 -07:00
|
|
|
name: Union[int, NString]
|
2017-09-18 03:01:48 -07:00
|
|
|
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))
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Cell(name)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
name (Union[NString, int, None]): name, "CellName reference"
|
|
|
|
number, or reuse modal
|
|
|
|
magnification (real_t): Magnification factor
|
|
|
|
angle (real_t): Rotation, degrees counterclockwise
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (repetition_t or None): Repetition, if any
|
|
|
|
flip (bool): Whether to perform reflection about the x-axis.
|
|
|
|
"""
|
|
|
|
name: Union[NString, int, None] = None
|
|
|
|
magnification: Optional[real_t] = None
|
|
|
|
angle: Optional[real_t] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
flip: bool
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
flip: bool,
|
2020-04-18 03:03:35 -07:00
|
|
|
name: Union[NString, str, int] = None,
|
|
|
|
magnification: Optional[real_t] = None,
|
|
|
|
angle: Optional[real_t] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
flip: Whether to perform reflection about the x-axis.
|
|
|
|
name: `NString`, an int specifying a `CellName` reference number,
|
|
|
|
or `None` (reuse modal).
|
|
|
|
magnification: Magnification factor. Default `None` (use modal).
|
|
|
|
angle: Rotation angle in degrees, counterclockwise.
|
|
|
|
Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Placement(flip, name, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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:
|
2020-04-18 01:31:36 -07:00
|
|
|
size += self.name.write(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
string (Union[AString, int, None]): None means reuse modal
|
|
|
|
layer (Optiona[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
string: Union[AString, int, None] = None
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
string: Union[AString, str, int] = None,
|
|
|
|
layer: Optional[int] = None,
|
|
|
|
datatype: Optional[int] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
string: Text content, or `TextString` reference number.
|
|
|
|
Default `None` (use modal).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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')
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Text(string, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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:
|
2020-04-18 01:31:36 -07:00
|
|
|
size += self.string.write(stream)
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
is_square (bool): `True` if this is a square.
|
|
|
|
If `True`, `height` must be `None`.
|
|
|
|
width (Optional[int]): `None` means reuse modal.
|
|
|
|
height (Optional[int]): Must be `None` if `is_square` is `True`.
|
|
|
|
If `is_square` is `False`, `None` means reuse modal.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
width: Optional[int] = None
|
|
|
|
height: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
is_square: bool = False
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
is_square: bool = False,
|
2020-04-18 03:03:35 -07:00
|
|
|
layer: Optional[int] = None,
|
|
|
|
datatype: Optional[int] = None,
|
|
|
|
width: Optional[int] = None,
|
|
|
|
height: Optional[int] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
is_square: `True` if this is a square. If `True`, `height` must
|
|
|
|
be `None`. Default `False`.
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
width: X-width. Default `None` (reuse modal).
|
|
|
|
height: Y-height. Default `None` (reuse modal, or use `width` if
|
|
|
|
square). Must be `None` if `is_square` is `True`.
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Rectangle(is_square, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]`
|
|
|
|
The list is an implicitly closed path, vertices are [int, int],
|
|
|
|
`None` means reuse modal.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
point_list: Optional[point_list_t] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
point_list: Optional[point_list_t] = None,
|
|
|
|
layer: Optional[int] = None,
|
|
|
|
datatype: Optional[int] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
point_list: List of vertices `[[x0, y0], [x1, y1], ...]`.
|
|
|
|
List forms an implicitly closed path.
|
|
|
|
Default `None` (reuse modal).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2019-09-08 02:25:04 -07:00
|
|
|
warn('Polygon with < 3 points')
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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')
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Polygon(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
point_list (Optional[point_list_t]): `[[x0, y0], [x1, y1], ...]`
|
|
|
|
Vertices are [int, int]; `None` means reuse modal.
|
|
|
|
half_width (Optional[int]): None means reuse modal
|
|
|
|
extension_start (Optional[Tuple]): None means reuse modal.
|
|
|
|
Tuple is of the form (`PathExtensionScheme`, Optional[int])
|
|
|
|
Second value is None unless using `PathExtensionScheme.Arbitrary`
|
|
|
|
Value determines extension past start point.
|
|
|
|
extension_end (Optional[Tuple]): Same form as `extension_end`.
|
|
|
|
Value determines extension past end point.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
point_list: Optional[point_list_t] = None
|
|
|
|
half_width: Optional[int] = None
|
|
|
|
extension_start: Optional[pathextension_t] = None
|
|
|
|
extension_end: Optional[pathextension_t] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
2020-04-18 03:03:35 -07:00
|
|
|
point_list: Optional[point_list_t] = None,
|
|
|
|
half_width: Optional[int] = None,
|
|
|
|
extension_start: Optional[pathextension_t] = None,
|
|
|
|
extension_end: Optional[pathextension_t] = None,
|
|
|
|
layer: Optional[int] = None,
|
|
|
|
datatype: Optional[int] = None,
|
|
|
|
x: Optional[int] = None,
|
|
|
|
y: Optional[int] = None,
|
|
|
|
repetition: Optional[repetition_t] = None):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
point_list: List of vertices `[[x0, y0], [x1, y1], ...]`.
|
|
|
|
Default `None` (reuse modal).
|
|
|
|
half_width: Half-width of the path. Default `None` (reuse modal).
|
|
|
|
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).
|
|
|
|
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).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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
|
|
|
|
|
2020-04-18 15:44:44 -07:00
|
|
|
def get_pathext(ext_scheme: int) -> Optional[pathextension_t]:
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2020-04-18 15:35:29 -07:00
|
|
|
else:
|
|
|
|
raise InvalidDataError('Invalid ext_scheme: {}'.format(ext_scheme))
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Path(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
delta_a (Optional[int]): 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 (Optional[int]): 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 (Optional[int]): Bounding box x-width, None means reuse modal.
|
|
|
|
height (Optional[int]): Bounding box y-height, None means reuse modal.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
width: Optional[int] = None
|
|
|
|
height: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
delta_a: int = 0
|
|
|
|
delta_b: int = 0
|
|
|
|
is_vertical: bool
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
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.
|
|
|
|
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).
|
|
|
|
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).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
width: X-width of bounding box. Default `None` (reuse modal).
|
|
|
|
height: Y-height of bounding box. Default `None` (reuse modal)
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if dimensions are impossible.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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:
|
2018-07-21 13:48:36 -07:00
|
|
|
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))
|
2017-09-18 03:01:48 -07:00
|
|
|
else:
|
2018-07-21 13:48:36 -07:00
|
|
|
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))
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Trapezoid(is_vertical, **optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
ctrapezoid_type (Optional[int]): see OASIS spec for details, None means reuse modal.
|
|
|
|
width (Optional[int]): Bounding box x-width, None means reuse modal.
|
|
|
|
height (Optional[int]): Bounding box y-height, None means reuse modal.
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
ctrapezoid_type: Optional[int] = None
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
width: Optional[int] = None
|
|
|
|
height: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
ctrapezoid_type: CTrapezoid type; see OASIS format
|
|
|
|
documentation. Default `None` (reuse modal).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
width: X-width of bounding box. Default `None` (reuse modal).
|
|
|
|
height: Y-height of bounding box. Default `None` (reuse modal)
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if dimensions are invalid.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2020-04-18 15:43:25 -07:00
|
|
|
self.check_valid()
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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')
|
|
|
|
|
2020-04-18 15:43:25 -07:00
|
|
|
self.check_valid()
|
|
|
|
|
2017-09-18 03:01:48 -07:00
|
|
|
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')
|
|
|
|
|
2020-04-18 15:43:25 -07:00
|
|
|
self.check_valid()
|
|
|
|
|
2017-09-18 03:01:48 -07:00
|
|
|
@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)
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = CTrapezoid(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-04-18 15:43:25 -07:00
|
|
|
def check_valid(self):
|
|
|
|
ctrapezoid_type = self.ctrapezoid_type
|
|
|
|
width = self.width
|
|
|
|
height = self.height
|
|
|
|
|
|
|
|
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 width is not None and height is not None:
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
2017-09-18 03:01:48 -07:00
|
|
|
class Circle(Record):
|
|
|
|
"""
|
|
|
|
Circle record (ID 27)
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Attributes:
|
|
|
|
radius (Optional[int]): None means reuse modal
|
|
|
|
layer (Optional[int]): None means reuse modal
|
|
|
|
datatype (Optional[int]): None means reuse modal
|
|
|
|
x (Optional[int]): x-offset, None means reuse modal
|
|
|
|
y (Optional[int]): y-offset, None means reuse modal
|
|
|
|
repetition (Optional[repetition_t]): Repetition, if any
|
|
|
|
"""
|
|
|
|
layer: Optional[int] = None
|
|
|
|
datatype: Optional[int] = None
|
|
|
|
x: Optional[int] = None
|
|
|
|
y: Optional[int] = None
|
|
|
|
repetition: Optional[repetition_t] = None
|
|
|
|
radius: Optional[int] = None
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
radius: int = None,
|
|
|
|
layer: int = None,
|
|
|
|
datatype: int = None,
|
|
|
|
x: int = None,
|
|
|
|
y: int = None,
|
|
|
|
repetition: repetition_t = None):
|
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
radius: Radius. Default `None` (reuse modal).
|
|
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
|
|
x: X-offset. Default `None` (use modal).
|
|
|
|
y: Y-offset. Default `None` (use modal).
|
|
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if dimensions are invalid.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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':
|
2018-07-21 13:49:45 -07:00
|
|
|
if record_id != 27:
|
2017-09-18 03:01:48 -07:00
|
|
|
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')
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
optional: Dict[str, Any] = {}
|
2017-09-18 03:01:48 -07:00
|
|
|
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)
|
2018-07-21 13:46:22 -07:00
|
|
|
record = Circle(**optional)
|
|
|
|
logger.debug('Record ending at 0x{:x}:\n {}'.format(stream.tell(), record))
|
|
|
|
return record
|
2017-09-18 03:01:48 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
def adjust_repetition(record, modals: Modals):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
Merge the record's repetition entry with the one in the modals
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: Record to read or modify.
|
|
|
|
modals: Modals to read or modify.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if a `ReuseRepetition` can't be filled
|
|
|
|
from the modals.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
def adjust_field(record, r_field: str, modals: Modals, m_field: str):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Merge `record.r_field` with `modals.m_field`
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: `Record` to read or modify.
|
|
|
|
r_field: Attr of record to access.
|
|
|
|
modals: `Modals` to read or modify.
|
|
|
|
m_field: Attr of modals to access.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if both fields are `None`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
def adjust_coordinates(record, modals: Modals, mx_field: str, my_field: str):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
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.
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: `Record` to read or modify.
|
|
|
|
modals: `Modals` to read or modify.
|
|
|
|
mx_field: Attr of modals corresponding to `record.x`
|
|
|
|
my_field: Attr of modals corresponding to `record.y`
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Raises:
|
|
|
|
InvalidDataError: if both fields are `None`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
2020-04-18 15:38:52 -07:00
|
|
|
def dedup_repetition(record, modals: Modals):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
Deduplicate the record's repetition entry with the one in the modals.
|
|
|
|
Update the one in the modals if they are different.
|
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: `Record` to read or modify.
|
|
|
|
modals: `Modals` to read or modify.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidDataError: if a `ReuseRepetition` can't be filled
|
|
|
|
from the modals.
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
def dedup_field(record, r_field: str, modals: Modals, m_field: str):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
Deduplicate `record.r_field` using `modals.m_field`
|
|
|
|
Update the `modals.m_field` if they are different.
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: `Record` to read or modify.
|
|
|
|
r_field: Attr of record to access.
|
|
|
|
modals: `Modals` to read or modify.
|
|
|
|
m_field: Attr of modals to access.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
InvalidDataError: if both fields are `None`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
r = getattr(record, r_field)
|
|
|
|
m = getattr(modals, m_field)
|
|
|
|
if r is not None:
|
2020-04-18 03:00:36 -07:00
|
|
|
if _USE_NUMPY and m_field in ('polygon_point_list', 'path_point_list'):
|
|
|
|
equal = numpy.array_equal(m, r)
|
|
|
|
else:
|
|
|
|
equal = m is not None and m == r
|
|
|
|
|
|
|
|
if equal:
|
2017-09-18 03:01:48 -07:00
|
|
|
setattr(record, r_field, None)
|
|
|
|
else:
|
|
|
|
setattr(modals, m_field, r)
|
|
|
|
elif m is None:
|
|
|
|
raise InvalidDataError('Unfillable field')
|
|
|
|
|
|
|
|
|
2020-04-18 15:38:52 -07:00
|
|
|
def dedup_coordinates(record, modals: Modals, mx_field: str, my_field: str):
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
2020-04-18 03:03:35 -07:00
|
|
|
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.
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Args:
|
|
|
|
record: `Record` to read or modify.
|
|
|
|
modals: `Modals` to read or modify.
|
|
|
|
mx_field: Attr of modals corresponding to `record.x`
|
|
|
|
my_field: Attr of modals corresponding to `record.y`
|
2017-09-18 03:01:48 -07:00
|
|
|
|
2020-04-18 03:03:35 -07:00
|
|
|
Raises:
|
|
|
|
InvalidDataError: if both fields are `None`
|
2017-09-18 03:01:48 -07:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|