initial commit
This commit is contained in:
		
						commit
						034d5fbcb7
					
				
							
								
								
									
										200
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					# klamath README
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`klamath` is a Python module for reading and writing to the GDSII file format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The goal is to keep this library simple:
 | 
				
			||||||
 | 
					- Map data types directly wherever possible.
 | 
				
			||||||
 | 
					    * Presents an accurate representation of what is saved to the file.
 | 
				
			||||||
 | 
					    * Avoids excess copies / allocations for speed.
 | 
				
			||||||
 | 
					    * No "automatic" error checking, except when casting datatypes.
 | 
				
			||||||
 | 
					        If data integrity checks are provided at all, they must be
 | 
				
			||||||
 | 
					        explicitly run by the caller.
 | 
				
			||||||
 | 
					- Low-level functionality is first-class.
 | 
				
			||||||
 | 
					    * Meant for use-cases where the caller wants to read or write
 | 
				
			||||||
 | 
					         individual GDS records.
 | 
				
			||||||
 | 
					    * Offers complete control over the written file.
 | 
				
			||||||
 | 
					- Opinionated and limited high-level functionality.
 | 
				
			||||||
 | 
					    * Discards or ignores rarely-encountered data types.
 | 
				
			||||||
 | 
					    * Keeps functions simple and reusable.
 | 
				
			||||||
 | 
					    * Only de/encodes the file format, doesn't provide tools to modify
 | 
				
			||||||
 | 
					        the data itself.
 | 
				
			||||||
 | 
					    * Still requires explicit values for most fields.
 | 
				
			||||||
 | 
					- No compilation
 | 
				
			||||||
 | 
					    * Uses `numpy` for speed, since it's commonly available / pre-built.
 | 
				
			||||||
 | 
					    * Building this library should not require a compiler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`klamath` was built to provide a fast and versatile GDS interface for
 | 
				
			||||||
 | 
					 [masque](https://mpxd.net/code/jan/masque), which provides higher-level
 | 
				
			||||||
 | 
					 tools for working with hierarchical design data and supports multiple
 | 
				
			||||||
 | 
					 file formats.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Alternatives
 | 
				
			||||||
 | 
					- [gdspy](https://github.com/heitzmann/gdspy)
 | 
				
			||||||
 | 
					    * Provides abstractions and methods for working with design data
 | 
				
			||||||
 | 
					        outside of the I/O process (e.g. polygon clipping).
 | 
				
			||||||
 | 
					    * Requires compilation (C++) to build from source.
 | 
				
			||||||
 | 
					    * Focused on high-level API
 | 
				
			||||||
 | 
					- [python-gdsii](https://pypi.org/project/python-gdsii)
 | 
				
			||||||
 | 
					    * Pure-python implementation. Can easily be altered to use `numpy`
 | 
				
			||||||
 | 
					        for speed, but is limited by object allocation overhead.
 | 
				
			||||||
 | 
					    * Focused on high-level API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Links
 | 
				
			||||||
 | 
					- [Source repository](https://mpxd.net/code/jan/klamath)
 | 
				
			||||||
 | 
					- [PyPI](https://pypi.org/project/klamath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requirements:
 | 
				
			||||||
 | 
					* python >= 3.7 (written and tested with 3.8)
 | 
				
			||||||
 | 
					* numpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install with pip:
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pip3 install klamath
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Alternatively, install from git
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pip3 install git+https://mpxd.net/code/jan/klamath.git@release
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Examples
 | 
				
			||||||
 | 
					### Low-level
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Filter which polygons are read based on layer:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python3
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					import klamath
 | 
				
			||||||
 | 
					from klamath import records
 | 
				
			||||||
 | 
					from klamath.record import Record
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_polygons(stream, filter_layer_tuple=(4, 5)):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Given a stream positioned at the start of a record,
 | 
				
			||||||
 | 
					     return the vertices of all BOUNDARY records which match
 | 
				
			||||||
 | 
					     the provided `filter_layer_tuple`, up to the next
 | 
				
			||||||
 | 
					     ENDSTR record.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    polys = []
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        stream.seek(size, io.SEEK_CUR)      # skip to next header
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if tag == records.ENDEL.tag:
 | 
				
			||||||
 | 
					            break                           # If ENDEL, we are done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if tag != records.BOUNDARY.tag:
 | 
				
			||||||
 | 
					            continue                        # Skip until we find a BOUNDARY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        layer = records.LAYER.skip_and_read(stream)[0]  # skip to LAYER
 | 
				
			||||||
 | 
					        dtype = records.DATATYPE.read(stream)[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (layer, dtype) != filter_layer_tuple:
 | 
				
			||||||
 | 
					            continue                        # Skip reading XY unless layer matches
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        xy = XY.read(stream).reshape(-1, 2)
 | 
				
			||||||
 | 
					        polys.append(xy)
 | 
				
			||||||
 | 
					    return polys
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### High-level
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Write an example GDS file:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python3
 | 
				
			||||||
 | 
					import klamath
 | 
				
			||||||
 | 
					from klamath.elements import Boundary, Text, Path, Reference
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream = file.open('example.gds', 'wb')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header = klamath.library.FileHeader(
 | 
				
			||||||
 | 
					                name=b'example',
 | 
				
			||||||
 | 
					                meters_per_db_unit=1e-9,      # 1 nm DB unit
 | 
				
			||||||
 | 
					                user_units_per_db_unit=1e-3)  # 1 um (1000nm) display unit
 | 
				
			||||||
 | 
					header.write(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elements_A = [
 | 
				
			||||||
 | 
					    Boundary(layer=(4, 18),
 | 
				
			||||||
 | 
					             xy=[[0, 0], [10, 0], [10, 20], [0, 20], [0, 0]],
 | 
				
			||||||
 | 
					             properties={1: b'prop1string', 2: b'some other string'}),
 | 
				
			||||||
 | 
					    Text(layer=(5, 5),
 | 
				
			||||||
 | 
					         xy=[[5, 10]],
 | 
				
			||||||
 | 
					         string=b'center position',
 | 
				
			||||||
 | 
					         properties={},        # Remaining args are set to default values
 | 
				
			||||||
 | 
					         presentation=0,       #   and will be omitted when writing
 | 
				
			||||||
 | 
					         angle_deg=0,
 | 
				
			||||||
 | 
					         invert_y=False,
 | 
				
			||||||
 | 
					         width=0,
 | 
				
			||||||
 | 
					         path_type=0,
 | 
				
			||||||
 | 
					         mag=1),
 | 
				
			||||||
 | 
					    Path(layer=(4, 20),
 | 
				
			||||||
 | 
					         xy=[[0, 0], [10, 10], [0, 20]],
 | 
				
			||||||
 | 
					         path_type=0,
 | 
				
			||||||
 | 
					         width=0,
 | 
				
			||||||
 | 
					         extension=(0, 0),     # ignored since path_type=0
 | 
				
			||||||
 | 
					         properties={}),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					klamath.library.write(stream, name=b'my_struct', elements=elements_A)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elements_top = [
 | 
				
			||||||
 | 
					    Reference(struct_name=b'my_struct',
 | 
				
			||||||
 | 
					              xy=[[30, 30]],
 | 
				
			||||||
 | 
					              colrow=None,   # not an array
 | 
				
			||||||
 | 
					              angle_deg=0,
 | 
				
			||||||
 | 
					              invert_y=True,
 | 
				
			||||||
 | 
					              mag=1.5,
 | 
				
			||||||
 | 
					              properties={}),
 | 
				
			||||||
 | 
					    Reference(struct_name=b'my_struct',
 | 
				
			||||||
 | 
					              colrow=(3, 2),                   # 3x2 array at (0, 50)
 | 
				
			||||||
 | 
					              xy=[[0, 50], [60, 50], [30, 50]] #   with basis vectors
 | 
				
			||||||
 | 
					              angle_deg=30,                    #   [20, 0] and [0, 30]
 | 
				
			||||||
 | 
					              invert_y=False,
 | 
				
			||||||
 | 
					              mag=1,
 | 
				
			||||||
 | 
					              properties={}),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					klamath.library.write(stream, name=b'top', elements=elements_top)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					klamath.records.ENDLIB.write(stream)
 | 
				
			||||||
 | 
					stream.close()
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Read back the file:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python3
 | 
				
			||||||
 | 
					import klamath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream = file.open('example.gds', 'rb')
 | 
				
			||||||
 | 
					header = klamath.library.FileHeader.read(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					structs = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct = klamath.library.try_read_struct(stream)
 | 
				
			||||||
 | 
					while struct is not None:
 | 
				
			||||||
 | 
					    name, elements = struct
 | 
				
			||||||
 | 
					    structs[name] = elements
 | 
				
			||||||
 | 
					    struct = klamath.library.try_read_struct(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream.close()
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Read back a single struct by name:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```python3
 | 
				
			||||||
 | 
					import klamath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream = file.open('example.gds', 'rb')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header = klamath.library.FileHeader.read(stream)
 | 
				
			||||||
 | 
					struct_positions = klamath.library.scan_structs(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream.seek(struct_positions[b'my_struct'])
 | 
				
			||||||
 | 
					elements_A = klamath.library.try_read_struct(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					stream.close()
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										1
									
								
								klamath/VERSION
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								klamath/VERSION
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					0.1
 | 
				
			||||||
							
								
								
									
										42
									
								
								klamath/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								klamath/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					`klamath` is a Python module for reading and writing to the GDSII file format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The goal is to keep this library simple:
 | 
				
			||||||
 | 
					- Map data types directly wherever possible.
 | 
				
			||||||
 | 
					    * Presents an accurate representation of what is saved to the file.
 | 
				
			||||||
 | 
					    * Avoids excess copies / allocations for speed.
 | 
				
			||||||
 | 
					    * No "automatic" error checking, except when casting datatypes.
 | 
				
			||||||
 | 
					        If data integrity checks are provided at all, they must be
 | 
				
			||||||
 | 
					        explicitly run by the caller.
 | 
				
			||||||
 | 
					- Low-level functionality is first-class.
 | 
				
			||||||
 | 
					    * Meant for use-cases where the caller wants to read or write
 | 
				
			||||||
 | 
					         individual GDS records.
 | 
				
			||||||
 | 
					    * Offers complete control over the written file.
 | 
				
			||||||
 | 
					- Opinionated and limited high-level functionality.
 | 
				
			||||||
 | 
					    * Discards or ignores rarely-encountered data types.
 | 
				
			||||||
 | 
					    * Keeps functions simple and reusable.
 | 
				
			||||||
 | 
					    * Only de/encodes the file format, doesn't provide tools to modify
 | 
				
			||||||
 | 
					        the data itself.
 | 
				
			||||||
 | 
					    * Still requires explicit values for most fields.
 | 
				
			||||||
 | 
					- No compilation
 | 
				
			||||||
 | 
					    * Uses `numpy` for speed, since it's commonly available / pre-built.
 | 
				
			||||||
 | 
					    * Building this library should not require a compiler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`klamath` was built to provide a fast and versatile GDS interface for
 | 
				
			||||||
 | 
					 [masque](https://mpxd.net/code/jan/masque), which provides higher-level
 | 
				
			||||||
 | 
					 tools for working with hierarchical design data and supports multiple
 | 
				
			||||||
 | 
					 file formats.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import pathlib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import basic
 | 
				
			||||||
 | 
					from . import record
 | 
				
			||||||
 | 
					from . import records
 | 
				
			||||||
 | 
					from . import elements
 | 
				
			||||||
 | 
					from . import library
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__author__ = 'Jan Petykiewicz'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f:
 | 
				
			||||||
 | 
					    __version__ = f.read().strip()
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/__init__.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/__init__.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/basic.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/basic.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/elements.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/elements.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/library.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/library.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/record.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/record.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/records.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/records.cpython-38.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/test_basic.cpython-38-PYTEST.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/test_basic.cpython-38-PYTEST.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								klamath/__pycache__/test_record.cpython-38-PYTEST.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								klamath/__pycache__/test_record.cpython-38-PYTEST.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										174
									
								
								klamath/basic.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								klamath/basic.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,174 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Functionality for encoding/decoding basic datatypes
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from typing import Sequence, BinaryIO, List
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import numpy        # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class KlamathError(Exception):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Parse functions
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					def parse_bitarray(data: bytes) -> int:
 | 
				
			||||||
 | 
					    if len(data) != 2:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
 | 
				
			||||||
 | 
					    (val,) = struct.unpack('>H', data)
 | 
				
			||||||
 | 
					    return val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_int2(data: bytes) -> numpy.ndarray:
 | 
				
			||||||
 | 
					    data_len = len(data)
 | 
				
			||||||
 | 
					    if data_len == 0 or (data_len % 2) != 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
 | 
				
			||||||
 | 
					    return numpy.frombuffer(data, dtype='>i2', count=data_len // 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_int4(data: bytes) -> numpy.ndarray:
 | 
				
			||||||
 | 
					    data_len = len(data)
 | 
				
			||||||
 | 
					    if data_len == 0 or (data_len % 4) != 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
 | 
				
			||||||
 | 
					    return numpy.frombuffer(data, dtype='>i4', count=data_len // 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def decode_real8(nums: numpy.ndarray) -> numpy.ndarray:
 | 
				
			||||||
 | 
					    """ Convert GDS REAL8 data to IEEE float64. """
 | 
				
			||||||
 | 
					    nums = nums.astype(numpy.uint64)
 | 
				
			||||||
 | 
					    neg = nums & 0x8000_0000_0000_0000
 | 
				
			||||||
 | 
					    exp = (nums >> 56) & 0x7f
 | 
				
			||||||
 | 
					    mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
 | 
				
			||||||
 | 
					    mant[neg != 0] *= -1
 | 
				
			||||||
 | 
					    return numpy.ldexp(mant, 4 * (exp - 64) - 56, dtype=numpy.float64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_real8(data: bytes) -> numpy.ndarray:
 | 
				
			||||||
 | 
					    data_len = len(data)
 | 
				
			||||||
 | 
					    if data_len == 0 or (data_len % 8) != 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
 | 
				
			||||||
 | 
					    ints = numpy.frombuffer(data, dtype='>u8', count=data_len // 8)
 | 
				
			||||||
 | 
					    return decode_real8(ints)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_ascii(data: bytes) -> bytes:
 | 
				
			||||||
 | 
					    if len(data) == 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Received empty ascii data.')
 | 
				
			||||||
 | 
					    if data[-1:] == b'\0':
 | 
				
			||||||
 | 
					        return data[:-1]
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_datetime(data: bytes) -> List[datetime]:
 | 
				
			||||||
 | 
					    """ Parse date/time data (12 byte blocks) """
 | 
				
			||||||
 | 
					    if len(data) == 0 or len(data) % 12 != 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.')
 | 
				
			||||||
 | 
					    dts = []
 | 
				
			||||||
 | 
					    for ii in range(0, len(data), 12):
 | 
				
			||||||
 | 
					        year, *date_parts = parse_int2(data[ii:ii+12])
 | 
				
			||||||
 | 
					        dts.append(datetime(year + 1900, *date_parts))
 | 
				
			||||||
 | 
					    return dts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Pack functions
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					def pack_bitarray(data: int) -> bytes:
 | 
				
			||||||
 | 
					    if data > 65535 or data < 0:
 | 
				
			||||||
 | 
					        raise KlamathError(f'bitarray data out of range: {data}')
 | 
				
			||||||
 | 
					    return struct.pack('>H', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pack_int2(data: Sequence[int]) -> bytes:
 | 
				
			||||||
 | 
					    arr = numpy.array(data)
 | 
				
			||||||
 | 
					    if (arr > 32767).any() or (arr < -32768).any():
 | 
				
			||||||
 | 
					        raise KlamathError(f'int2 data out of range: {arr}')
 | 
				
			||||||
 | 
					    return arr.astype('>i2').tobytes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pack_int4(data: Sequence[int]) -> bytes:
 | 
				
			||||||
 | 
					    arr = numpy.array(data)
 | 
				
			||||||
 | 
					    if (arr > 2147483647).any() or (arr < -2147483648).any():
 | 
				
			||||||
 | 
					        raise KlamathError(f'int4 data out of range: {arr}')
 | 
				
			||||||
 | 
					    return arr.astype('>i4').tobytes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
 | 
				
			||||||
 | 
					    """ Convert from float64 to GDS REAL8 representation. """
 | 
				
			||||||
 | 
					    # Split the bitfields
 | 
				
			||||||
 | 
					    ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
 | 
				
			||||||
 | 
					    sign = ieee & numpy.uint64(0x8000_0000_0000_0000)
 | 
				
			||||||
 | 
					    ieee_exp = (ieee >> numpy.uint64(52)).astype(numpy.int32) & numpy.int32(0x7ff)
 | 
				
			||||||
 | 
					    ieee_mant = ieee & numpy.uint64(0xf_ffff_ffff_ffff)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subnorm = (ieee_exp == 0) & (ieee_mant != 0)
 | 
				
			||||||
 | 
					    zero = (ieee_exp == 0) & (ieee_mant == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Convert exponent.
 | 
				
			||||||
 | 
					    #  * 16-based
 | 
				
			||||||
 | 
					    #  * +1 due to mantissa differences (1.xxxx in IEEE vs 0.1xxxxx in GDSII)
 | 
				
			||||||
 | 
					    exp16, rest = numpy.divmod(ieee_exp + 1 - 1023, 4)
 | 
				
			||||||
 | 
					    # Compensate exponent conversion
 | 
				
			||||||
 | 
					    comp = (rest != 0)
 | 
				
			||||||
 | 
					    exp16[comp] += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    shift = rest.copy().astype(numpy.int8)
 | 
				
			||||||
 | 
					    shift[comp] = 4 - rest[comp]
 | 
				
			||||||
 | 
					    shift -= 3      # account for gds bit position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # add leading one
 | 
				
			||||||
 | 
					    gds_mant_unshifted = ieee_mant + 0x10_0000_0000_0000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rshift = (shift > 0)
 | 
				
			||||||
 | 
					    gds_mant = numpy.empty_like(ieee_mant)
 | 
				
			||||||
 | 
					    gds_mant[~rshift] = gds_mant_unshifted[~rshift] << (-shift[~rshift]).astype(numpy.uint8)
 | 
				
			||||||
 | 
					    gds_mant[ rshift] = gds_mant_unshifted[ rshift] >> ( shift[ rshift]).astype(numpy.uint8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # add gds exponent bias
 | 
				
			||||||
 | 
					    exp16_biased = exp16 + 64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    neg_biased = (exp16_biased < 0)
 | 
				
			||||||
 | 
					    gds_mant[neg_biased] >>= (exp16_biased[neg_biased] * 4).astype(numpy.uint8)
 | 
				
			||||||
 | 
					    exp16_biased[neg_biased] = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    too_big = (exp16_biased > 0x7f) & ~(zero | subnorm)
 | 
				
			||||||
 | 
					    if too_big.any():
 | 
				
			||||||
 | 
					        raise KlamathError(f'Number(s) too big for real8 format: {fnums[too_big]}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    gds_exp = exp16_biased.astype(numpy.uint64) << 56
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    real8 = sign | gds_exp | gds_mant
 | 
				
			||||||
 | 
					    real8[zero] = 0
 | 
				
			||||||
 | 
					    real8[subnorm] = 0            # TODO handle subnormals
 | 
				
			||||||
 | 
					    real8[exp16_biased < -14] = 0 # number is too small
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return real8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pack_real8(data: Sequence[float]) -> bytes:
 | 
				
			||||||
 | 
					    return encode_real8(numpy.array(data)).astype('>u8').tobytes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pack_ascii(data: bytes) -> bytes:
 | 
				
			||||||
 | 
					    size = len(data)
 | 
				
			||||||
 | 
					    if size % 2 != 0:
 | 
				
			||||||
 | 
					        return data + b'\0'
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pack_datetime(data: Sequence[datetime]) -> bytes:
 | 
				
			||||||
 | 
					    """ Pack date/time data (12 byte blocks) """
 | 
				
			||||||
 | 
					    parts = sum(((d.year - 1900, d.month, d.day, d.hour, d.minute, d.second)
 | 
				
			||||||
 | 
					                 for d in data), start=())
 | 
				
			||||||
 | 
					    return pack_int2(parts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read(stream: BinaryIO, size: int) -> bytes:
 | 
				
			||||||
 | 
					    """ Read and check for failure """
 | 
				
			||||||
 | 
					    data = stream.read(size)
 | 
				
			||||||
 | 
					    if len(data) != size:
 | 
				
			||||||
 | 
					        raise EOFError
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
							
								
								
									
										489
									
								
								klamath/elements.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										489
									
								
								klamath/elements.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,489 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Functionality for reading/writing elements (geometry, text labels,
 | 
				
			||||||
 | 
					 structure references) and associated properties.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type
 | 
				
			||||||
 | 
					from abc import ABCMeta, abstractmethod
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import numpy        # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .basic import KlamathError
 | 
				
			||||||
 | 
					from .record import Record
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
 | 
				
			||||||
 | 
					from .records import DATATYPE, PATHTYPE, BOXTYPE, NODETYPE, TEXTTYPE
 | 
				
			||||||
 | 
					from .records import LAYER, XY, WIDTH, COLROW, PRESENTATION, STRING
 | 
				
			||||||
 | 
					from .records import STRANS, MAG, ANGLE, PROPATTR, PROPVALUE
 | 
				
			||||||
 | 
					from .records import ENDEL, BGNEXTN, ENDEXTN, SNAME
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					E = TypeVar('E', bound='Element')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					R = TypeVar('R', bound='Reference')
 | 
				
			||||||
 | 
					B = TypeVar('B', bound='Boundary')
 | 
				
			||||||
 | 
					P = TypeVar('P', bound='Path')
 | 
				
			||||||
 | 
					N = TypeVar('N', bound='Node')
 | 
				
			||||||
 | 
					T = TypeVar('T', bound='Text')
 | 
				
			||||||
 | 
					X = TypeVar('X', bound='Box')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Read element properties.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Assumes PROPATTR records have unique values.
 | 
				
			||||||
 | 
					    Stops reading after consuming ENDEL record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: Stream to read from.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        {propattr: b'propvalue'} mapping.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    properties = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					    while tag != ENDEL.tag:
 | 
				
			||||||
 | 
					        if tag == PROPATTR.tag:
 | 
				
			||||||
 | 
					            key = PROPATTR.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            value = PROPVALUE.read(stream)
 | 
				
			||||||
 | 
					            if key in properties:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Duplicate property key: {key!r}')
 | 
				
			||||||
 | 
					            properties[key]  = value
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					    return properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Write element properties.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is does _not_ write the ENDEL record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: Stream to write to.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    b = 0
 | 
				
			||||||
 | 
					    for key, value in properties.items():
 | 
				
			||||||
 | 
					        b += PROPATTR.write(stream, key)
 | 
				
			||||||
 | 
					        b += PROPVALUE.write(stream, value)
 | 
				
			||||||
 | 
					    return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Element(metaclass=ABCMeta):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Abstract method definition for GDS structure contents
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[E], stream: BinaryIO) -> E:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Read from a stream to construct this object.
 | 
				
			||||||
 | 
					        Consumes up to (and including) the ENDEL record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            Stream to read from.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Constructed object.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Write this element to a stream.
 | 
				
			||||||
 | 
					        Finishes with an ENDEL record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            Stream to write to.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Number of bytes written
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Reference(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing
 | 
				
			||||||
 | 
					      an instance of a structure (SREF / structure reference) or
 | 
				
			||||||
 | 
					      an array of instances      (AREF / array reference).
 | 
				
			||||||
 | 
					    Type is determined by the presence of the `colrow` tuple.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Transforms are applied to each individual instance (_not_
 | 
				
			||||||
 | 
					      to the instance's origin location or array vectors).
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('struct_name', 'invert_y', 'mag', 'angle_deg', 'xy', 'colrow', 'properties')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    struct_name: bytes
 | 
				
			||||||
 | 
					    """ Name of the structure being referenced. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    invert_y: bool
 | 
				
			||||||
 | 
					    """ Whether to mirror the pattern (negate y-values / flip across x-axis). Default False. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mag: float
 | 
				
			||||||
 | 
					    """ Scaling factor (default 1) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    angle_deg: float
 | 
				
			||||||
 | 
					    """ Rotation (degrees counterclockwise) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					     (For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
 | 
				
			||||||
 | 
					     (For AREF) 3 locations:
 | 
				
			||||||
 | 
					                [`offset`,
 | 
				
			||||||
 | 
					                 `offset + col_basis_vector * colrow[0]`,
 | 
				
			||||||
 | 
					                 `offset + row_basis_vector * colrow[1]`]
 | 
				
			||||||
 | 
					                which define the first instance's offset and the array's basis vectors.
 | 
				
			||||||
 | 
					        Note that many GDS implementations only support manhattan basis vectors, and some
 | 
				
			||||||
 | 
					          assume a certain axis mapping (e.g. x->columns, y->rows) and "reinterpret" the
 | 
				
			||||||
 | 
					          basis vectors to match it.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    colrow: Optional[Tuple[int, int]]
 | 
				
			||||||
 | 
					    """ Number of columns and rows (AREF) or None (SREF) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties associated with this reference. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[R], stream: BinaryIO) -> R:
 | 
				
			||||||
 | 
					        invert_y = False
 | 
				
			||||||
 | 
					        mag = 1
 | 
				
			||||||
 | 
					        angle_deg = 0
 | 
				
			||||||
 | 
					        colrow = None
 | 
				
			||||||
 | 
					        struct_name = SNAME.skip_and_read(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        while tag != XY.tag:
 | 
				
			||||||
 | 
					            if tag == STRANS.tag:
 | 
				
			||||||
 | 
					                strans = STRANS.read_data(stream, size)
 | 
				
			||||||
 | 
					                invert_y = bool(0x8000 & strans)
 | 
				
			||||||
 | 
					            elif tag == MAG.tag:
 | 
				
			||||||
 | 
					                mag = MAG.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == ANGLE.tag:
 | 
				
			||||||
 | 
					                angle_deg = ANGLE.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == COLROW.tag:
 | 
				
			||||||
 | 
					                colrow = COLROW.read_data(stream, size)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Unexpected tag {tag:04x}')
 | 
				
			||||||
 | 
					            size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        xy = XY.read_data(stream, size).reshape(-1, 2)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow,
 | 
				
			||||||
 | 
					                   invert_y=invert_y, mag=mag, angle_deg=angle_deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = 0
 | 
				
			||||||
 | 
					        if self.colrow is None:
 | 
				
			||||||
 | 
					            b += SREF.write(stream, None)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            b += AREF.write(stream, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        b += SNAME.write(stream, self.struct_name)
 | 
				
			||||||
 | 
					        if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
 | 
				
			||||||
 | 
					            b += STRANS.write(stream, int(self.invert_y) << 15)
 | 
				
			||||||
 | 
					            if self.mag != 1:
 | 
				
			||||||
 | 
					                b += MAG.write(stream, self.mag)
 | 
				
			||||||
 | 
					            if self.angle_deg !=0:
 | 
				
			||||||
 | 
					                b += ANGLE.write(stream, self.angle_deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.colrow is not None:
 | 
				
			||||||
 | 
					            b += COLROW.write(stream, self.colrow)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check(self) -> None:
 | 
				
			||||||
 | 
					        if self.colrow is not None:
 | 
				
			||||||
 | 
					            if self.xy.size != 6:
 | 
				
			||||||
 | 
					                raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if self.xy.size != 2:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Boundary(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing a Boundary element.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('layer', 'xy', 'properties')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    layer: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ (layer, data_type) tuple """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Ordered vertices of the shape. First and last points should be identical. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties for the element. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[B], stream: BinaryIO) -> B:
 | 
				
			||||||
 | 
					        layer = LAYER.skip_and_read(stream)[0]
 | 
				
			||||||
 | 
					        dtype = DATATYPE.read(stream)[0]
 | 
				
			||||||
 | 
					        xy = XY.read(stream).reshape(-1, 2)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(layer=(layer, dtype), xy=xy, properties=properties)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = BOUNDARY.write(stream, None)
 | 
				
			||||||
 | 
					        b += LAYER.write(stream, self.layer[0])
 | 
				
			||||||
 | 
					        b += DATATYPE.write(stream, self.layer[1])
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Path(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing a Path element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If `path_type < 4`, `extension` values are not written.
 | 
				
			||||||
 | 
					    During read, `exension` defaults to (0, 0) even if unused.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    layer: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ (layer, data_type) tuple """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    path_type: int
 | 
				
			||||||
 | 
					    """ End-cap type (0: flush, 1: circle, 2: square, 4: custom) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    width: int
 | 
				
			||||||
 | 
					    """ Path width """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    extension: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ Extension when using path_type=4. Ignored otherwise. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Path centerline coordinates """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties for the element. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[P], stream: BinaryIO) -> P:
 | 
				
			||||||
 | 
					        path_type = 0
 | 
				
			||||||
 | 
					        width = 0
 | 
				
			||||||
 | 
					        bgn_ext = 0
 | 
				
			||||||
 | 
					        end_ext = 0
 | 
				
			||||||
 | 
					        layer = LAYER.skip_and_read(stream)[0]
 | 
				
			||||||
 | 
					        dtype = DATATYPE.read(stream)[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        while tag != XY.tag:
 | 
				
			||||||
 | 
					            if tag == PATHTYPE.tag:
 | 
				
			||||||
 | 
					                path_type = PATHTYPE.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == WIDTH.tag:
 | 
				
			||||||
 | 
					                width = WIDTH.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == BGNEXTN.tag:
 | 
				
			||||||
 | 
					                bgn_ext = BGNEXTN.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == ENDEXTN.tag:
 | 
				
			||||||
 | 
					                end_ext = ENDEXTN.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Unexpected tag {tag:04x}')
 | 
				
			||||||
 | 
					            size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        xy = XY.read_data(stream, size).reshape(-1, 2)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(layer=(layer, dtype), xy=xy,
 | 
				
			||||||
 | 
					                   properties=properties, extension=(bgn_ext, end_ext),
 | 
				
			||||||
 | 
					                   path_type=path_type, width=width)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = PATH.write(stream, None)
 | 
				
			||||||
 | 
					        b += LAYER.write(stream, self.layer[0])
 | 
				
			||||||
 | 
					        b += DATATYPE.write(stream, self.layer[1])
 | 
				
			||||||
 | 
					        if self.path_type != 0:
 | 
				
			||||||
 | 
					            b += PATHTYPE.write(stream, self.path_type)
 | 
				
			||||||
 | 
					        if self.width != 0:
 | 
				
			||||||
 | 
					            b += WIDTH.write(stream, self.width)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.path_type < 4:
 | 
				
			||||||
 | 
					            bgn_ext, end_ext = self.extension
 | 
				
			||||||
 | 
					            if bgn_ext != 0:
 | 
				
			||||||
 | 
					                b += BGNEXTN.write(stream, bgn_ext)
 | 
				
			||||||
 | 
					            if end_ext != 0:
 | 
				
			||||||
 | 
					                b += ENDEXTN.write(stream, end_ext)
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Box(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing a Box element. Rarely used.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('layer', 'xy', 'properties')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    layer: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ (layer, box_type) tuple """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Box coordinates (5 pairs) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties for the element. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[X], stream: BinaryIO) -> X:
 | 
				
			||||||
 | 
					        layer = LAYER.skip_and_read(stream)
 | 
				
			||||||
 | 
					        dtype = BOXTYPE.read(stream)
 | 
				
			||||||
 | 
					        xy = XY.read(stream).reshape(-1, 2)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(layer=(layer, dtype), xy=xy, properties=properties)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = BOX.write(stream, None)
 | 
				
			||||||
 | 
					        b += LAYER.write(stream, self.layer[0])
 | 
				
			||||||
 | 
					        b += BOXTYPE.write(stream, self.layer[1])
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Node(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing a Node element. Rarely used.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('layer', 'xy', 'properties')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    layer: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ (layer, node_type) tuple """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """ 1-50 pairs of coordinates. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties for the element. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[N], stream: BinaryIO) -> N:
 | 
				
			||||||
 | 
					        layer = LAYER.skip_and_read(stream)
 | 
				
			||||||
 | 
					        dtype = NODETYPE.read(stream)
 | 
				
			||||||
 | 
					        xy = XY.read(stream).reshape(-1, 2)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(layer=(layer, dtype), xy=xy, properties=properties)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = NODE.write(stream, None)
 | 
				
			||||||
 | 
					        b += LAYER.write(stream, self.layer[0])
 | 
				
			||||||
 | 
					        b += NODETYPE.write(stream, self.layer[1])
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class Text(Element):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Datastructure representing a Node element. Rarely used.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
 | 
				
			||||||
 | 
					                 'width', 'invert_y', 'mag', 'angle_deg', 'string')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    layer: Tuple[int, int]
 | 
				
			||||||
 | 
					    """ (layer, node_type) tuple """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    presentation: int
 | 
				
			||||||
 | 
					    """ Bit array. Default all zeros.
 | 
				
			||||||
 | 
					        bits 0-1: 00 left/01 center/10 right
 | 
				
			||||||
 | 
					        bits 2-3: 00 top/01 middle/10 bottom
 | 
				
			||||||
 | 
					        bits 4-5: font number
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    path_type: int
 | 
				
			||||||
 | 
					    """ Default 0 """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    width: int
 | 
				
			||||||
 | 
					    """ Default 0 """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    invert_y: bool
 | 
				
			||||||
 | 
					    """ Vertical inversion. Default False. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mag: float
 | 
				
			||||||
 | 
					    """ Scaling factor. Default 1. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    angle_deg: float
 | 
				
			||||||
 | 
					    """ Rotation (ccw). Default 0. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xy: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Position (1 pair only) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    string: bytes
 | 
				
			||||||
 | 
					    """ Text content """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    properties: Dict[int, bytes]
 | 
				
			||||||
 | 
					    """ Properties for the element. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[T], stream: BinaryIO) -> T:
 | 
				
			||||||
 | 
					        path_type = 0
 | 
				
			||||||
 | 
					        presentation = 0
 | 
				
			||||||
 | 
					        invert_y = False
 | 
				
			||||||
 | 
					        width = 0
 | 
				
			||||||
 | 
					        mag = 1
 | 
				
			||||||
 | 
					        angle_deg = 0
 | 
				
			||||||
 | 
					        layer = LAYER.skip_and_read(stream)
 | 
				
			||||||
 | 
					        dtype = TEXTTYPE.read(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        while tag != XY.tag:
 | 
				
			||||||
 | 
					            if tag == PRESENTATION.tag:
 | 
				
			||||||
 | 
					                presentation = PRESENTATION.read_data(stream, size)
 | 
				
			||||||
 | 
					            elif tag == PATHTYPE.tag:
 | 
				
			||||||
 | 
					                path_type = PATHTYPE.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == WIDTH.tag:
 | 
				
			||||||
 | 
					                width = WIDTH.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == STRANS.tag:
 | 
				
			||||||
 | 
					                strans = STRANS.read_data(stream, size)
 | 
				
			||||||
 | 
					                invert_y = bool(0x8000 & strans)
 | 
				
			||||||
 | 
					            elif tag == MAG.tag:
 | 
				
			||||||
 | 
					                mag = MAG.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            elif tag == ANGLE.tag:
 | 
				
			||||||
 | 
					                angle_deg = ANGLE.read_data(stream, size)[0]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Unexpected tag {tag:04x}')
 | 
				
			||||||
 | 
					            size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        xy = XY.read_data(stream, size).reshape(-1, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        string = STRING.read(stream)
 | 
				
			||||||
 | 
					        properties = read_properties(stream)
 | 
				
			||||||
 | 
					        return cls(layer=(layer, dtype), xy=xy, properties=properties,
 | 
				
			||||||
 | 
					                   string=string, presentation=presentation, path_type=path_type,
 | 
				
			||||||
 | 
					                   width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        b = TEXT.write(stream, None)
 | 
				
			||||||
 | 
					        b += LAYER.write(stream, self.layer[0])
 | 
				
			||||||
 | 
					        b += TEXTTYPE.write(stream, self.layer[1])
 | 
				
			||||||
 | 
					        if self.presentation != 0:
 | 
				
			||||||
 | 
					            b += PRESENTATION.write(stream, self.presentation)
 | 
				
			||||||
 | 
					        if self.path_type != 0:
 | 
				
			||||||
 | 
					            b += PATHTYPE.write(stream, self.path_type)
 | 
				
			||||||
 | 
					        if self.width != 0:
 | 
				
			||||||
 | 
					            b += WIDTH.write(stream, self.width)
 | 
				
			||||||
 | 
					        if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
 | 
				
			||||||
 | 
					            b += STRANS.write(stream, int(self.invert_y) << 15)
 | 
				
			||||||
 | 
					            if self.mag != 1:
 | 
				
			||||||
 | 
					                b += MAG.write(stream, self.mag)
 | 
				
			||||||
 | 
					            if self.angle_deg !=0:
 | 
				
			||||||
 | 
					                b += ANGLE.write(stream, self.angle_deg)
 | 
				
			||||||
 | 
					        b += XY.write(stream, self.xy)
 | 
				
			||||||
 | 
					        b += write_properties(stream, self.properties)
 | 
				
			||||||
 | 
					        b += ENDEL.write(stream, None)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
							
								
								
									
										187
									
								
								klamath/library.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								klamath/library.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					File-level read/write functionality.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .basic import KlamathError
 | 
				
			||||||
 | 
					from .record import Record
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .records import HEADER, BGNLIB, ENDLIB, UNITS, LIBNAME
 | 
				
			||||||
 | 
					from .records import BGNSTR, STRNAME, ENDSTR
 | 
				
			||||||
 | 
					from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
 | 
				
			||||||
 | 
					from .elements import Element, Reference, Text, Box, Boundary, Path, Node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FH = TypeVar('FH', bound='FileHeader')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass
 | 
				
			||||||
 | 
					class FileHeader:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Representation of the GDS file header.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    File header records: HEADER BGNLIB LIBNAME UNITS
 | 
				
			||||||
 | 
					       Optional record are ignored if present and never written.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Version is assumed to be `600`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    name: bytes
 | 
				
			||||||
 | 
					    """ Library name """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_units_per_db_unit: float
 | 
				
			||||||
 | 
					    """ Number of user units in one database unit """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    meters_per_db_unit: float
 | 
				
			||||||
 | 
					    """ Number of meters in one database unit """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mod_time: datetime = datetime(1900, 1, 1)
 | 
				
			||||||
 | 
					    """ Last-modified time """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acc_time: datetime = datetime(1900, 1, 1)
 | 
				
			||||||
 | 
					    """ Last-accessed time """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[FH], stream: BinaryIO) -> FH:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Read and construct a header from the provided stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            stream: Seekable stream to read from
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            FileHeader object
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        version = HEADER.read(stream)[0]
 | 
				
			||||||
 | 
					        if version != 600:
 | 
				
			||||||
 | 
					            raise KlamathError(f'Got GDS version {version}, expected 600')
 | 
				
			||||||
 | 
					        mod_time, acc_time = BGNLIB.read(stream)
 | 
				
			||||||
 | 
					        name = LIBNAME.skip_and_read(stream)
 | 
				
			||||||
 | 
					        uu, dbu = UNITS.skip_and_read(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cls(mod_time=mod_time, acc_time=acc_time, name=name,
 | 
				
			||||||
 | 
					                   user_units_per_db_unit=uu, meters_per_db_unit=dbu)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write(self, stream: BinaryIO) -> int:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Write the header to a stream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            stream: Stream to write to
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            number of bytes written
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        b = HEADER.write(stream, 600)
 | 
				
			||||||
 | 
					        b += BGNLIB.write(stream, (self.mod_time, self.acc_time))
 | 
				
			||||||
 | 
					        b += LIBNAME.write(stream, self.name)
 | 
				
			||||||
 | 
					        b += UNITS.write(stream, (self.user_units_per_db_unit, self.meters_per_db_unit))
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Scan through a GDS file, building a table of
 | 
				
			||||||
 | 
					      {b'structure_name': byte_offset}.
 | 
				
			||||||
 | 
					    The intent of this function is to enable random access
 | 
				
			||||||
 | 
					     and/or partial (structure-by-structure) reads.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: Seekable stream to read from. Should be positioned
 | 
				
			||||||
 | 
					                before the first structure record, but possibly
 | 
				
			||||||
 | 
					                already past the file header.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    positions = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					    while tag != ENDLIB.tag:
 | 
				
			||||||
 | 
					        stream.seek(size, io.SEEK_CUR)
 | 
				
			||||||
 | 
					        if tag == BGNSTR.tag:
 | 
				
			||||||
 | 
					            name = STRNAME.read(stream)
 | 
				
			||||||
 | 
					            if name in positions:
 | 
				
			||||||
 | 
					                raise KlamathError(f'Duplicate structure name: {name!r}')
 | 
				
			||||||
 | 
					            positions[name] = stream.tell()
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return positions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Skip to the next structure and attempt to read it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: Seekable stream to read from.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        (name, elements) if a structure was found.
 | 
				
			||||||
 | 
					        None if no structure was found before the end of the library.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not BGNSTR.skip_past(stream):
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    name = STRNAME.read(stream)
 | 
				
			||||||
 | 
					    elements = read_elements(stream)
 | 
				
			||||||
 | 
					    return name, elements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write_struct(stream: BinaryIO,
 | 
				
			||||||
 | 
					                 name: bytes,
 | 
				
			||||||
 | 
					                 elements: List[Element],
 | 
				
			||||||
 | 
					                 cre_time: datetime = datetime(1900, 1, 1),
 | 
				
			||||||
 | 
					                 mod_time: datetime = datetime(1900, 1, 1),
 | 
				
			||||||
 | 
					                 ) -> int:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Write a structure to the provided stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        name: Structure name (ascii-encoded).
 | 
				
			||||||
 | 
					        elements: List of Elements containing the geometry and text in this struct.
 | 
				
			||||||
 | 
					        cre_time: Creation time (optional).
 | 
				
			||||||
 | 
					        mod_time: Modification time (optional).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Return:
 | 
				
			||||||
 | 
					        Number of bytes written
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    b = BGNSTR.write(stream, (cre_time, mod_time))
 | 
				
			||||||
 | 
					    b += STRNAME.write(stream, name)
 | 
				
			||||||
 | 
					    b += sum(el.write(stream) for el in elements)
 | 
				
			||||||
 | 
					    b += ENDSTR.write(stream, None)
 | 
				
			||||||
 | 
					    return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_elements(stream: BinaryIO) -> List[Element]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Read elements from the stream until an ENDSTR
 | 
				
			||||||
 | 
					      record is encountered. The ENDSTR record is also
 | 
				
			||||||
 | 
					      consumed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: Seekable stream to read from.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        List of element objects.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    data: List[Element] = []
 | 
				
			||||||
 | 
					    size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					    while tag != ENDSTR.tag:
 | 
				
			||||||
 | 
					        if tag == BOUNDARY.tag:
 | 
				
			||||||
 | 
					            data.append(Boundary.read(stream))
 | 
				
			||||||
 | 
					        elif tag == PATH.tag:
 | 
				
			||||||
 | 
					            data.append(Path.read(stream))
 | 
				
			||||||
 | 
					        elif tag == NODE.tag:
 | 
				
			||||||
 | 
					            data.append(Node.read(stream))
 | 
				
			||||||
 | 
					        elif tag == BOX.tag:
 | 
				
			||||||
 | 
					            data.append(Box.read(stream))
 | 
				
			||||||
 | 
					        elif tag == TEXT.tag:
 | 
				
			||||||
 | 
					            data.append(Text.read(stream))
 | 
				
			||||||
 | 
					        elif tag == SREF.tag:
 | 
				
			||||||
 | 
					            data.append(Reference.read(stream))
 | 
				
			||||||
 | 
					        elif tag == AREF.tag:
 | 
				
			||||||
 | 
					            data.append(Reference.read(stream))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # don't care, skip
 | 
				
			||||||
 | 
					            stream.seek(size, io.SEEK_CUR)
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
							
								
								
									
										0
									
								
								klamath/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								klamath/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										208
									
								
								klamath/record.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								klamath/record.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,208 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Generic record-level read/write functionality.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from typing import Optional, Sequence, BinaryIO
 | 
				
			||||||
 | 
					from typing import TypeVar, List, Tuple, ClassVar, Type
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from abc import ABCMeta, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import numpy        # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .basic import KlamathError
 | 
				
			||||||
 | 
					from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
 | 
				
			||||||
 | 
					from .basic import pack_int2, pack_int4, pack_real8, pack_datetime, pack_bitarray
 | 
				
			||||||
 | 
					from .basic import parse_ascii, pack_ascii, read
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_RECORD_HEADER_FMT = struct.Struct('>HH')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
 | 
				
			||||||
 | 
					    record_size = data_size + 4
 | 
				
			||||||
 | 
					    if record_size > 0xFFFF:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Record size is too big: {record_size}')
 | 
				
			||||||
 | 
					    header = _RECORD_HEADER_FMT.pack(record_size, tag)
 | 
				
			||||||
 | 
					    return stream.write(header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Read a record's header (size and tag).
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        stream: stream to read from
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        data_size: size of data (not including header)
 | 
				
			||||||
 | 
					        tag: Record type tag
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    header = read(stream, 4)
 | 
				
			||||||
 | 
					    record_size, tag = _RECORD_HEADER_FMT.unpack(header)
 | 
				
			||||||
 | 
					    if record_size < 4:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Record size is too small: {record_size} @ pos 0x{stream.tell():x}')
 | 
				
			||||||
 | 
					    if record_size % 2:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Record size is odd: {record_size} @ pos 0x{stream.tell():x}')
 | 
				
			||||||
 | 
					    data_size = record_size - 4     # substract header size
 | 
				
			||||||
 | 
					    return data_size, tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def expect_record(stream: BinaryIO, tag: int) -> int:
 | 
				
			||||||
 | 
					    data_size, actual_tag = read_record_header(stream)
 | 
				
			||||||
 | 
					    if tag != actual_tag:
 | 
				
			||||||
 | 
					        raise KlamathError(f'Unexpected record! Got tag {actual_tag:04x}, expected {tag:04x}')
 | 
				
			||||||
 | 
					    return data_size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					R = TypeVar('R', bound='Record')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Record(metaclass=ABCMeta):
 | 
				
			||||||
 | 
					    tag: ClassVar[int] = -1
 | 
				
			||||||
 | 
					    expected_size: ClassVar[Optional[int]] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_size(cls, size: int):
 | 
				
			||||||
 | 
					        if cls.expected_size is not None and size != cls.expected_size:
 | 
				
			||||||
 | 
					            raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_data(cls, data):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data) -> bytes:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def read_header(stream: BinaryIO) -> Tuple[int, int]:
 | 
				
			||||||
 | 
					        return read_record_header(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def write_header(cls, stream: BinaryIO, data_size: int) -> int:
 | 
				
			||||||
 | 
					        return write_record_header(stream, data_size, cls.tag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def skip_past(cls, stream: BinaryIO) -> bool:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Skip to the end of the next occurence of this record.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            stream: Seekable stream to read from.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            True if the record was encountered and skipped.
 | 
				
			||||||
 | 
					            False if the end of the library was reached.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        from .records import ENDLIB
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        while tag != cls.tag:
 | 
				
			||||||
 | 
					            stream.seek(size, io.SEEK_CUR)
 | 
				
			||||||
 | 
					            if tag == ENDLIB.tag:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        stream.seek(size, io.SEEK_CUR)
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def skip_and_read(cls, stream: BinaryIO):
 | 
				
			||||||
 | 
					        size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        while tag != cls.tag:
 | 
				
			||||||
 | 
					            stream.seek(size, io.SEEK_CUR)
 | 
				
			||||||
 | 
					            size, tag = Record.read_header(stream)
 | 
				
			||||||
 | 
					        data = cls.read_data(stream, size)
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read(cls: Type[R], stream: BinaryIO):
 | 
				
			||||||
 | 
					        size = expect_record(stream, cls.tag)
 | 
				
			||||||
 | 
					        data = cls.read_data(stream, size)
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def write(cls, stream: BinaryIO, data) -> int:
 | 
				
			||||||
 | 
					        data_bytes = cls.pack_data(data)
 | 
				
			||||||
 | 
					        b = cls.write_header(stream, len(data_bytes))
 | 
				
			||||||
 | 
					        b += stream.write(data_bytes)
 | 
				
			||||||
 | 
					        return b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NoDataRecord(Record):
 | 
				
			||||||
 | 
					    expected_size: ClassVar[Optional[int]] = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> None:
 | 
				
			||||||
 | 
					        stream.read(size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: None) -> bytes:
 | 
				
			||||||
 | 
					        if data is not None:
 | 
				
			||||||
 | 
					            raise KlamathError('?? Packing {data} into NoDataRecord??')
 | 
				
			||||||
 | 
					        return b''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BitArrayRecord(Record):
 | 
				
			||||||
 | 
					    expected_size: ClassVar[Optional[int]] = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> int:
 | 
				
			||||||
 | 
					        return parse_bitarray(read(stream, 2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: int) -> bytes:
 | 
				
			||||||
 | 
					        return pack_bitarray(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Int2Record(Record):
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        return parse_int2(read(stream, size))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: Sequence[int]) -> bytes:
 | 
				
			||||||
 | 
					        return pack_int2(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Int4Record(Record):
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        return parse_int4(read(stream, size))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: Sequence[int]) -> bytes:
 | 
				
			||||||
 | 
					        return pack_int4(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Real8Record(Record):
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        return parse_real8(read(stream, size))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: Sequence[int]) -> bytes:
 | 
				
			||||||
 | 
					        return pack_real8(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ASCIIRecord(Record):
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> bytes:
 | 
				
			||||||
 | 
					        return parse_ascii(read(stream, size))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: bytes) -> bytes:
 | 
				
			||||||
 | 
					        return pack_ascii(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DateTimeRecord(Record):
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
 | 
				
			||||||
 | 
					        return parse_datetime(read(stream, size))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def pack_data(cls, data: Sequence[datetime]) -> bytes:
 | 
				
			||||||
 | 
					        return pack_datetime(data)
 | 
				
			||||||
							
								
								
									
										333
									
								
								klamath/records.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								klamath/records.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,333 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Record type and tag definitions
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from typing import Sequence
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
 | 
				
			||||||
 | 
					from .record import ASCIIRecord, DateTimeRecord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HEADER(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x0002
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BGNLIB(DateTimeRecord):
 | 
				
			||||||
 | 
					    tag = 0x0102
 | 
				
			||||||
 | 
					    expected_size = 6 * 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LIBNAME(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x0206
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UNITS(Real8Record):
 | 
				
			||||||
 | 
					    """ (user_units_per_db_unit, db_units_per_meter) """
 | 
				
			||||||
 | 
					    tag = 0x0305
 | 
				
			||||||
 | 
					    expected_size = 8 * 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ENDLIB(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BGNSTR(DateTimeRecord):
 | 
				
			||||||
 | 
					    tag = 0x0502
 | 
				
			||||||
 | 
					    expected_size = 6 * 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STRNAME(ASCIIRecord):
 | 
				
			||||||
 | 
					    """ Legal characters are `?A-Za-z0-9_$` """
 | 
				
			||||||
 | 
					    tag = 0x0606
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ENDSTR(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0700
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BOUNDARY(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0800
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PATH(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0900
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SREF(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0a00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AREF(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0b00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TEXT(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x0c00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LAYER(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x0d02
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DATATYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x0e02
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WIDTH(Int4Record):
 | 
				
			||||||
 | 
					    tag = 0x0f03
 | 
				
			||||||
 | 
					    expected_size = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class XY(Int4Record):
 | 
				
			||||||
 | 
					    tag = 0x1003
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ENDEL(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x1100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SNAME(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x1206
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class COLROW(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x1302
 | 
				
			||||||
 | 
					    expected_size = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NODE(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x1500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TEXTTYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x1602
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PRESENTATION(BitArrayRecord):
 | 
				
			||||||
 | 
					    tag = 0x1701
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SPACING(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x1802        #Not sure about 02; Unused
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STRING(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x1906
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STRANS(BitArrayRecord):
 | 
				
			||||||
 | 
					    tag = 0x1a01
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MAG(Real8Record):
 | 
				
			||||||
 | 
					    tag = 0x1b05
 | 
				
			||||||
 | 
					    expected_size = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ANGLE(Real8Record):
 | 
				
			||||||
 | 
					    tag = 0x1c05
 | 
				
			||||||
 | 
					    expected_size = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UINTEGER(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x1d02    #Unused; not sure about 02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class USTRING(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x1e06      #Unused; not sure about 06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class REFLIBS(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x1f06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_size(cls, size: int):
 | 
				
			||||||
 | 
					        if size != 0 and size % 44 != 0:
 | 
				
			||||||
 | 
					            raise Exception(f'Expected size to be multiple of 44, got {size}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FONTS(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x2006
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_size(cls, size: int):
 | 
				
			||||||
 | 
					        if size != 0 and size % 44 != 0:
 | 
				
			||||||
 | 
					            raise Exception(f'Expected size to be multiple of 44, got {size}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PATHTYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2102
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GENERATIONS(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2202
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_data(cls, data: Sequence[int]):
 | 
				
			||||||
 | 
					        if len(data) != 1:
 | 
				
			||||||
 | 
					            raise Exception(f'Expected exactly one integer, got {data}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ATTRTABLE(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x2306
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_size(cls, size: int):
 | 
				
			||||||
 | 
					        if size > 44:
 | 
				
			||||||
 | 
					            raise Exception(f'Expected size <= 44, got {size}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STYPTABLE(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x2406        #UNUSED, not sure about 06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STRTYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2502        #UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ELFLAGS(BitArrayRecord):
 | 
				
			||||||
 | 
					    tag = 0x2601
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ELKEY(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2703        # UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LINKTYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2803        # UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LINKKEYS(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2903        # UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NODETYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2a02
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PROPATTR(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2b02
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PROPVALUE(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x2c06
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BOX(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x2d00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BOXTYPE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x2e02
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PLEX(Int4Record):
 | 
				
			||||||
 | 
					    tag = 0x2f03
 | 
				
			||||||
 | 
					    expected_size = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BGNEXTN(Int4Record):
 | 
				
			||||||
 | 
					    tag = 0x3003
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ENDEXTN(Int4Record):
 | 
				
			||||||
 | 
					    tag = 0x3103
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TAPENUM(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3202
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TAPECODE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3302
 | 
				
			||||||
 | 
					    expected_size = 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class STRCLASS(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3401        # UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RESERVED(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3503        # UNUSED
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FORMAT(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3602
 | 
				
			||||||
 | 
					    expected_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def check_data(cls, data: Sequence[int]):
 | 
				
			||||||
 | 
					        if len(data) != 1:
 | 
				
			||||||
 | 
					            raise Exception(f'Expected exactly one integer, got {data}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MASK(ASCIIRecord):
 | 
				
			||||||
 | 
					    """ List of layers and dtypes """
 | 
				
			||||||
 | 
					    tag = 0x3706
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ENDMASKS(NoDataRecord):
 | 
				
			||||||
 | 
					    """ End of MASKS records """
 | 
				
			||||||
 | 
					    tag = 0x3800
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LIBDIRSIZE(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3902
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SRFNAME(ASCIIRecord):
 | 
				
			||||||
 | 
					    tag = 0x3a06
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LIBSECUR(Int2Record):
 | 
				
			||||||
 | 
					    tag = 0x3b02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BORDER(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x3c00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SOFTFENCE(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x3d00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HARDFENCE(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x3f00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SOFTWIRE(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x3f00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HARDWIRE(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PATHPORT(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NODEPORT(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class USERCONSTRAINT(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4300
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SPACERERROR(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CONTACT(NoDataRecord):
 | 
				
			||||||
 | 
					    tag = 0x4500
 | 
				
			||||||
							
								
								
									
										119
									
								
								klamath/test_basic.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								klamath/test_basic.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					import struct
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest       # type: ignore
 | 
				
			||||||
 | 
					import numpy        # type: ignore
 | 
				
			||||||
 | 
					from numpy.testing import assert_array_equal        # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii
 | 
				
			||||||
 | 
					from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii
 | 
				
			||||||
 | 
					from .basic import decode_real8, encode_real8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .basic import KlamathError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_parse_bitarray():
 | 
				
			||||||
 | 
					    assert(parse_bitarray(b'59') == 13625)
 | 
				
			||||||
 | 
					    assert(parse_bitarray(b'\0\0') == 0)
 | 
				
			||||||
 | 
					    assert(parse_bitarray(b'\xff\xff') == 65535)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 4 bytes (too long)
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_bitarray(b'4321')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # empty data
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_bitarray(b'')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_parse_int2():
 | 
				
			||||||
 | 
					    assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # odd length
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_int2(b'54321')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # empty data
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_int2(b'')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_parse_int4():
 | 
				
			||||||
 | 
					    assert_array_equal(parse_int4(b'4321'), (875770417,))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # length % 4 != 0
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_int4(b'654321')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # empty data
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_int4(b'')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_decode_real8():
 | 
				
			||||||
 | 
					    # zeroes
 | 
				
			||||||
 | 
					    assert(decode_real8(numpy.array([0x0])) == 0)
 | 
				
			||||||
 | 
					    assert(decode_real8(numpy.array([1<<63])) == 0) # negative
 | 
				
			||||||
 | 
					    assert(decode_real8(numpy.array([0xff << 56])) == 0) # denormalized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0)
 | 
				
			||||||
 | 
					    assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_parse_real8():
 | 
				
			||||||
 | 
					    packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000)
 | 
				
			||||||
 | 
					    assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # length % 8 != 0
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_real8(b'0987654321')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # empty data
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_real8(b'')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_parse_ascii():
 | 
				
			||||||
 | 
					    # empty data
 | 
				
			||||||
 | 
					    with pytest.raises(KlamathError):
 | 
				
			||||||
 | 
					        parse_ascii(b'')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert(parse_ascii(b'12345') == b'12345')
 | 
				
			||||||
 | 
					    assert(parse_ascii(b'12345\0') == b'12345') # strips trailing null byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pack_bitarray():
 | 
				
			||||||
 | 
					    packed = pack_bitarray(321)
 | 
				
			||||||
 | 
					    assert(len(packed) == 2)
 | 
				
			||||||
 | 
					    assert(packed == struct.pack('>H', 321))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pack_int2():
 | 
				
			||||||
 | 
					    packed = pack_int2((3, 2, 1))
 | 
				
			||||||
 | 
					    assert(len(packed) == 3*2)
 | 
				
			||||||
 | 
					    assert(packed == struct.pack('>3h', 3, 2, 1))
 | 
				
			||||||
 | 
					    assert(pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pack_int4():
 | 
				
			||||||
 | 
					    packed = pack_int4((3, 2, 1))
 | 
				
			||||||
 | 
					    assert(len(packed) == 3*4)
 | 
				
			||||||
 | 
					    assert(packed == struct.pack('>3l', 3, 2, 1))
 | 
				
			||||||
 | 
					    assert(pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_encode_real8():
 | 
				
			||||||
 | 
					    assert(encode_real8(numpy.array([0.0])) == 0)
 | 
				
			||||||
 | 
					    arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
 | 
				
			||||||
 | 
					    assert_array_equal(decode_real8(encode_real8(arr)), arr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pack_real8():
 | 
				
			||||||
 | 
					    reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
 | 
				
			||||||
 | 
					    packed = pack_real8(reals)
 | 
				
			||||||
 | 
					    assert(len(packed) == len(reals) * 8)
 | 
				
			||||||
 | 
					    assert_array_equal(parse_real8(packed), reals)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pack_ascii():
 | 
				
			||||||
 | 
					    assert(pack_ascii(b'4321') == b'4321')
 | 
				
			||||||
 | 
					    assert(pack_ascii(b'321') == b'321\0')
 | 
				
			||||||
							
								
								
									
										65
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from setuptools import setup, find_packages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with open('README.md', 'r') as f:
 | 
				
			||||||
 | 
					    long_description = f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with open('klamath/VERSION', 'r') as f:
 | 
				
			||||||
 | 
					    version = f.read().strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setup(name='klamath',
 | 
				
			||||||
 | 
					      version=version,
 | 
				
			||||||
 | 
					      description='GDSII format reader/writer',
 | 
				
			||||||
 | 
					      long_description=long_description,
 | 
				
			||||||
 | 
					      long_description_content_type='text/markdown',
 | 
				
			||||||
 | 
					      author='Jan Petykiewicz',
 | 
				
			||||||
 | 
					      author_email='anewusername@gmail.com',
 | 
				
			||||||
 | 
					      url='https://mpxd.net/code/jan/klamath',
 | 
				
			||||||
 | 
					      packages=find_packages(),
 | 
				
			||||||
 | 
					      package_data={
 | 
				
			||||||
 | 
					          'klamath': ['VERSION',
 | 
				
			||||||
 | 
					                     'py.typed',
 | 
				
			||||||
 | 
					                     ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      install_requires=[
 | 
				
			||||||
 | 
					            'numpy',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      classifiers=[
 | 
				
			||||||
 | 
					            'Programming Language :: Python :: 3',
 | 
				
			||||||
 | 
					            'Development Status :: 4 - Beta',
 | 
				
			||||||
 | 
					            'Intended Audience :: Developers',
 | 
				
			||||||
 | 
					            'Intended Audience :: Information Technology',
 | 
				
			||||||
 | 
					            'Intended Audience :: Manufacturing',
 | 
				
			||||||
 | 
					            'Intended Audience :: Science/Research',
 | 
				
			||||||
 | 
					            'License :: OSI Approved :: GNU General Public License v3',
 | 
				
			||||||
 | 
					            'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      keywords=[
 | 
				
			||||||
 | 
					          'layout',
 | 
				
			||||||
 | 
					          'design',
 | 
				
			||||||
 | 
					          'CAD',
 | 
				
			||||||
 | 
					          'EDA',
 | 
				
			||||||
 | 
					          'electronics',
 | 
				
			||||||
 | 
					          'photonics',
 | 
				
			||||||
 | 
					          'IC',
 | 
				
			||||||
 | 
					          'mask',
 | 
				
			||||||
 | 
					          'pattern',
 | 
				
			||||||
 | 
					          'drawing',
 | 
				
			||||||
 | 
					          'lithography',
 | 
				
			||||||
 | 
					          'litho',
 | 
				
			||||||
 | 
					          'geometry',
 | 
				
			||||||
 | 
					          'geometric',
 | 
				
			||||||
 | 
					          'polygon',
 | 
				
			||||||
 | 
					          'gds',
 | 
				
			||||||
 | 
					          'gdsii',
 | 
				
			||||||
 | 
					          'gds2',
 | 
				
			||||||
 | 
					          'stream',
 | 
				
			||||||
 | 
					          'vector',
 | 
				
			||||||
 | 
					          'freeform',
 | 
				
			||||||
 | 
					          'manhattan',
 | 
				
			||||||
 | 
					          'angle',
 | 
				
			||||||
 | 
					          'Calma',
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user