#!/usr/bin/env python3 # coding=utf-8 # # Copyright (c) 2024 jonathan.neuhauser@outlook.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Parse binary CGM files """ from dataclasses import dataclass, fields, is_dataclass from io import BufferedIOBase, BytesIO import struct from typing import ( ClassVar, Dict, List, Optional, Tuple, Type, Union, get_type_hints, get_origin, get_args, ) import warnings import cgm_enums import inkex class FunctionRegistry(dict): def register(self, key): def decorator(func): self[key] = func return func return decorator class BinaryCGMCommandParser: type_registry: Dict[Tuple[int, int], Type] = {} unsigned_readers = FunctionRegistry() signed_readers = FunctionRegistry() real_readers = FunctionRegistry() readers = FunctionRegistry() sizers = FunctionRegistry() def __init__(self, data: Union[bytes, BufferedIOBase]): if isinstance(data, bytes): data = BytesIO(data) # The stream we currently operate on. For long form commands its # easier to temporarily exchange this reference self.stream = data # The main stream of the file self.outer_stream = data # Reader functions for basic data types, where precision can be changed # in the file. self.integer_precision = 16 self.index_precision = 16 self.real_type = cgm_enums.RealTypeEnum.FIXED_POINT self.real_precision = (16, 16) self.vdc_integer_precision = 16 self.vdc_real_type = cgm_enums.RealTypeEnum.FIXED_POINT self.vdc_real_precision = (16, 16) self.float_precision = (9, 23) self.colour_component_precision = 8 self.colour_index_precision = 8 self.vdc_type = cgm_enums.VDCTypeEnum.INTEGER self.vdc_real_type = cgm_enums.RealTypeEnum.FIXED_POINT self.vdc_real_precision = (16, 16) self.colour_model = cgm_enums.ColourModelEnum.RGB self.colour_selection_mode = cgm_enums.ColourSelectionModeEnum.INDEXED self.line_width_specification_mode = cgm_enums.WidthSpecificationModeEnum.SCALED self.marker_size_specification_mode = ( cgm_enums.WidthSpecificationModeEnum.SCALED ) self.edge_width_specification_mode = cgm_enums.WidthSpecificationModeEnum.SCALED self.interior_style_specification_mode = ( cgm_enums.WidthSpecificationModeEnum.ABSOLUTE ) @staticmethod def register_target_datatype(cls): if cls.id is not None: BinaryCGMCommandParser.type_registry[cls.id] = cls def read(self, bytes): res = self.stream.read(bytes) if len(res) != bytes: raise ValueError("EOF") return res def read_padded(self, length): # An uneven length of a command is padded with a null byte if length % 2 == 1: arg_bytes = self.read(length + 1) assert arg_bytes[-1] == 0 return arg_bytes[:-1] return self.read(length) @unsigned_readers.register(8) def read_u8(self) -> int: return int.from_bytes(self.read(1), byteorder="big") @unsigned_readers.register(16) def read_u16(self) -> int: return int.from_bytes(self.read(2), byteorder="big") @unsigned_readers.register(24) def read_u24(self) -> int: return int.from_bytes(self.read(3), byteorder="big") @unsigned_readers.register(32) def read_u32(self) -> int: return int.from_bytes(self.read(4), byteorder="big") @signed_readers.register(8) def read_i8(self) -> int: return int.from_bytes(self.read(1), byteorder="big", signed=True) @signed_readers.register(16) @readers.register("E") def read_i16(self) -> int: return int.from_bytes(self.read(2), byteorder="big", signed=True) @signed_readers.register(24) def read_i24(self) -> int: return int.from_bytes(self.read(3), byteorder="big", signed=True) @signed_readers.register(32) def read_i32(self) -> int: return int.from_bytes(self.read(4), byteorder="big", signed=True) @real_readers.register((cgm_enums.RealTypeEnum.FLOATING_POINT, 9, 23)) def read_float(self) -> float: return struct.unpack(">f", self.read(4))[0] @real_readers.register((cgm_enums.RealTypeEnum.FLOATING_POINT, 12, 52)) def read_double(self) -> float: return struct.unpack(">d", self.read(8))[0] @real_readers.register((cgm_enums.RealTypeEnum.FIXED_POINT, 16, 16)) def read_fixed_single(self) -> float: # Some sample files appear to not be using # two's complement for signed integers if they occur in a flot. # IMO that's incorrect according to the specification. return self.read_i16() + self.read_u16() / 2**16 @real_readers.register((cgm_enums.RealTypeEnum.FIXED_POINT, 32, 32)) def read_fixed_double(self) -> float: return self.read_i32() + self.read_u32() / 2**32 @readers.register("S") @readers.register("D") @readers.register("SF") def read_str(self) -> str: """The first byte indicates the size. If the size is 255, the first two bytes indicate the size. From the new size, the first bit is a continuation flag and the remaining 15 bits are the size""" data: bytes size = self.read_u8() if size != 0xFF: data = self.read(size) else: # Long-form string self.stream.seek(0) _data = bytearray() while True: cur_size = self.read_u16() length = cur_size & 0b0111111111111111 _data.extend(self.read(length)) if not cur_size & 0b1000000000000000: break data = bytes(_data) # TODO implement proper string encoding return data.decode("latin-1") def get_single_value(self, type_str: Union[str, Tuple[str]]): if type_str in self.readers: return BinaryCGMCommandParser.readers[type_str](self) else: raise ValueError(f"Unknown data type {type_str}") def get_value( self, type_str: Union[str, Tuple[str, ...]], target_types: Union[type, Tuple[type]], length: Optional[int] = None, ): """This function reads a value from the stream. The result can either be: - a primitive type (e.g. "I", -> int) - a tuple of primitive types (e.g. ("I", "P"), (, ) -> (int, Point)) - a dataclass (e.g. ("P", "E"), -> PolygonLineSetEntry) in this case the type_str applies to the fields of the dataclass, and the target types of those fields are inferred from the type hint of the dataclass field - a list of any of the previous (e.g. ("I", "R"), typing.List[, ] -> List[Tuple[int, float]]) length must be defined for lists This just calls get_value with the argument of the List typehint, and collects the result until we've read length bytes """ res = None initial_pos = self.stream.tell() if isinstance(type_str, str) and isinstance(target_types, type): if target_types == str and length == 0: return "" res = self.get_single_value(type_str) if type_str not in ("P", "CO", "CD", "CI"): res = target_types(res) elif isinstance(type_str, tuple): if get_origin(target_types) == tuple: target_types = get_args(target_types) if isinstance(target_types, tuple): assert len(target_types) == len(type_str) res = tuple( self.get_value(i, t) for t, i in zip(target_types, type_str) ) elif is_dataclass(target_types): res = target_types( *self.get_value( type_str, tuple( get_type_hints(target_types)[t.name] for t in fields(target_types) ), ) ) if res is None and get_origin(target_types) == list: assert length is not None target_type = get_args(target_types)[0] try: field_size = self.get_size(type_str) # Assert that the remaining bytes fit into the list without residue assert length % field_size == 0 except ValueError: # just an assertion here, continue pass res = [] while True: if (self.stream.tell() - initial_pos) >= length: break res.append(self.get_value(type_str, target_type)) assert res is not None, "invalid specification for get_value" # assert isinstance(res, target_types), "bad return type" return res @readers.register("IX") def read_index(self): return BinaryCGMCommandParser.signed_readers[self.index_precision](self) @readers.register("SI") @readers.register("I") def read_signed(self): return BinaryCGMCommandParser.signed_readers[self.integer_precision](self) @readers.register("VDC") def read_vdc(self): return ( BinaryCGMCommandParser.signed_readers[self.vdc_integer_precision](self) if self.vdc_type == cgm_enums.VDCTypeEnum.INTEGER else BinaryCGMCommandParser.real_readers[ (self.vdc_real_type, *self.vdc_real_precision) ](self) ) @readers.register("R") def read_real(self): return BinaryCGMCommandParser.real_readers[ (self.real_type, *self.real_precision) ](self) @readers.register("F") def read_floating(self): return BinaryCGMCommandParser.real_readers[ (cgm_enums.RealTypeEnum.FLOATING_POINT, *self.float_precision) ](self) @readers.register("CI") def read_indexed_colour(self): return cgm_enums.IndexColour( self.signed_readers[self.colour_index_precision](self) ) @readers.register("CCO") def read_colour_component(self): return BinaryCGMCommandParser.unsigned_readers[self.colour_component_precision]( self ) @readers.register("CD") def read_direct_colour(self): result = cgm_enums.DirectColour( *( self.read_colour_component() for _ in range( 4 if self.colour_model == cgm_enums.ColourModelEnum.CMYK else 3 ) ) ) result.colour_model = self.colour_model return result @readers.register("CO") def read_colour(self): if self.colour_selection_mode == cgm_enums.ColourSelectionModeEnum.DIRECT: return self.read_direct_colour() else: return self.read_indexed_colour() @readers.register("P") def read_point(self): return cgm_enums.Point(self.read_vdc(), self.read_vdc()) def read_size(self, mode: cgm_enums.WidthSpecificationModeEnum): if mode == cgm_enums.WidthSpecificationModeEnum.ABSOLUTE: return self.read_vdc() return self.read_real() @readers.register("LSS") def read_line_size(self): return self.read_size(self.line_width_specification_mode) @readers.register("MSS") def read_marker_size(self): return self.read_size(self.marker_size_specification_mode) @readers.register("ESS") def read_edge_size(self): return self.read_size(self.edge_width_specification_mode) @readers.register("ISS") def read_interior_style_size(self): return self.read_size(self.interior_style_specification_mode) # SIZING FUNCTIONS def get_size(self, type_str: Union[str, Tuple[str, ...]]): if type_str in BinaryCGMCommandParser.sizers: return BinaryCGMCommandParser.sizers[type_str](self) if isinstance(type_str, tuple): return sum(self.get_size(i) for i in type_str) else: raise ValueError(f"No size know for type {type_str}") @sizers.register("CI") def ci_size(self): return self.colour_index_precision // 8 @sizers.register("CCO") def cco_size(self): return self.colour_component_precision // 8 @sizers.register("CD") def cd_size(self): return self.cco_size() * ( 4 if self.colour_model == cgm_enums.ColourModelEnum.CMYK else 3 ) @sizers.register("IX") def ix_size(self): return self.index_precision // 8 @sizers.register("E") def e_size(self): return 2 @sizers.register("I") def i_size(self): return self.integer_precision // 8 @sizers.register("R") def r_size(self): return (self.real_precision[0] + self.real_precision[1]) // 8 @sizers.register("VDC") def vdc_size(self): return ( self.vdc_integer_precision // 8 if self.vdc_type == cgm_enums.VDCTypeEnum.INTEGER else (self.vdc_real_precision[0] + self.vdc_real_precision[1]) // 8 ) @sizers.register("P") def p_size(self): return 2 * self.vdc_size() @sizers.register("CO") def co_size(self): if self.colour_selection_mode == cgm_enums.ColourSelectionModeEnum.DIRECT: return self.cd_size() else: return self.ci_size() def ss_size(self, mode: cgm_enums.WidthSpecificationModeEnum): if mode == cgm_enums.WidthSpecificationModeEnum.ABSOLUTE: return self.vdc_size() return self.r_size() @sizers.register("LSS") def lss_size(self): return self.ss_size(self.line_width_specification_mode) @sizers.register("MSS") def marker_size(self): return self.ss_size(self.marker_size_specification_mode) @sizers.register("ESS") def edge_size(self): return self.ss_size(self.edge_width_specification_mode) @sizers.register("ISS") def interior_style_size(self): return self.ss_size(self.interior_style_specification_mode) def parse(self): while True: try: header = self.read_u16() except ValueError: return clss = header >> 12 obj_id = (header & 0b0000111111100000) >> 5 total_length = header & 0x1F if total_length == 31: # Long-form header args = bytearray() total_length = 0 while True: data = self.read_u16() length = data & 0b0111111111111111 total_length += length args.extend(self.read_padded(length)) if not data & 0b1000000000000000: break self.stream = BytesIO(bytes(args)) old_pos = self.stream.tell() interpreted = self.command_factory(clss, obj_id, total_length) new_pos = self.stream.tell() if ( new_pos - old_pos not in [total_length] + [total_length - 1] if total_length % 2 == 0 else [] ): warnings.warn( f"{interpreted.__class__.__name__}: Expected to read {total_length} bytes, but read {new_pos - old_pos}" ) new_pos = old_pos + total_length self.stream.seek(new_pos) if isinstance(interpreted, ParserModifierCommand): self.update_readers(interpreted) # Prepare the stream for the next entry if self.stream == self.outer_stream: if new_pos % 2 != 0: # Fields are always aligned with 2-byte words self.read(1) else: self.stream = self.outer_stream yield interpreted if isinstance(interpreted, EndMetafile): # Sometimes there are trailing characters afterwards, return return def parse_data(self, data: BytesIO): """Temporarily exchanges the stream by data to read primitives from it, using the same settings as the current stream""" outer_stream = self.outer_stream stream = self.stream self.outer_stream = data self.stream = data result = tuple(self.parse()) self.outer_stream = outer_stream self.stream = stream return result def update_readers(self, interpreted): if isinstance(interpreted, VdcType): self.vdc_type = interpreted.vdc_type elif isinstance(interpreted, IntegerPrecision): self.integer_precision = interpreted.integer_precision elif isinstance(interpreted, RealPrecision): self.real_type = interpreted.representation self.real_precision = ( interpreted.exponent_whole_size, interpreted.fraction_size, ) if interpreted.representation == cgm_enums.RealTypeEnum.FLOATING_POINT: self.float_precision = ( interpreted.exponent_whole_size, interpreted.fraction_size, ) elif isinstance(interpreted, IndexPrecision): self.index_precision = interpreted.index_precision elif isinstance(interpreted, ColourIndexPrecision): self.colour_index_precision = interpreted.colour_index_precision elif isinstance(interpreted, ColourPrecision): self.colour_component_precision = interpreted.colour_precision elif isinstance(interpreted, ColourSelectionMode): self.colour_selection_mode = interpreted.colour_selection_mode elif isinstance(interpreted, ColourModel): self.colour_model = interpreted.colour_model elif isinstance(interpreted, VDCIntegerPrecision): self.vdc_integer_precision = interpreted.integer_precision elif isinstance(interpreted, VDCRealPrecision): self.vdc_real_type = interpreted.representation self.vdc_real_precision = ( interpreted.exponent_whole_size, interpreted.fraction_size, ) elif isinstance(interpreted, LineWidthSpecificationMode): self.line_width_specification_mode = interpreted.size_specification elif isinstance(interpreted, EdgeWidthSpecificationMode): self.edge_width_specification_mode = interpreted.size_specification elif isinstance(interpreted, MarkerSizeSpecificationMode): self.marker_size_specification_mode = interpreted.size_specification elif isinstance(interpreted, InteriorStyleSpecificationMode): self.interior_style_specification_mode = interpreted.size_specification else: inkex.errormsg( f"Parser modifier command {interpreted} without associated action" ) def command_factory(self, group, command, length): cls = self.type_registry.get((group, command), UnknownCommand) result = cls.parse_binary(self, length) if isinstance(result, UnknownCommand): result.id = (group, command) return result @dataclass class CGMCommand: id: ClassVar[Tuple[int, int]] = (0, 0) binary_params: ClassVar[Tuple[Union[str, Tuple[str, ...]], ...]] = tuple() def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) BinaryCGMCommandParser.register_target_datatype(cls) @classmethod def get_binary_args( cls, stream: BinaryCGMCommandParser, length: int, ): initial_pos = stream.stream.tell() for i, (field, types) in enumerate(zip(fields(cls), cls.binary_params)): outer_type = get_type_hints(cls)[field.name] yield stream.get_value( types, outer_type, length - (stream.stream.tell() - initial_pos) ) @classmethod def parse_binary(cls, stream: BinaryCGMCommandParser, length): args = list(cls.get_binary_args(stream, length)) return cls(*args) 6 @dataclass class NoOp(CGMCommand): id = (0, 0) data: bytes @classmethod def parse_binary(cls, stream: BinaryCGMCommandParser, length): return cls(stream.read(length)) @dataclass class UnknownCommand(CGMCommand): data: bytes @classmethod def parse_binary(cls, stream: BinaryCGMCommandParser, length): return cls(stream.read(length)) @dataclass class BeginMetafile(CGMCommand): id = (0, 1) binary_params = ("SF",) name: str @dataclass class EndMetafile(CGMCommand): id = (0, 2) @dataclass class BeginPicture(CGMCommand): id = (0, 3) binary_params = ("SF",) name: str @dataclass class BeginPictureBody(CGMCommand): id = (0, 4) @dataclass class EndPicture(CGMCommand): id = (0, 5) # Metafile descriptor elements @dataclass class MetafileVersion(CGMCommand): id = (1, 1) binary_params = ("I",) version: int @dataclass class MetafileDescription(CGMCommand): id = (1, 2) binary_params = ("SF",) description: str class ParserModifierCommand(CGMCommand): pass @dataclass class VdcType(ParserModifierCommand): id = (1, 3) binary_params = ("E",) vdc_type: cgm_enums.VDCTypeEnum @dataclass class IntegerPrecision(ParserModifierCommand): id = (1, 4) binary_params = ("I",) integer_precision: int @dataclass class RealPrecision(ParserModifierCommand): id = (1, 5) binary_params = "E", "I", "I" representation: cgm_enums.RealTypeEnum exponent_whole_size: int fraction_size: int @dataclass class IndexPrecision(ParserModifierCommand): id = (1, 6) binary_params = ("I",) index_precision: int @dataclass class ColourPrecision(ParserModifierCommand): id = (1, 7) binary_params = ("I",) colour_precision: int @dataclass class ColourIndexPrecision(ParserModifierCommand): id = (1, 8) binary_params = ("I",) colour_index_precision: int @dataclass class MaximumColourIndex(CGMCommand): id = (1, 9) binary_params = ("CI",) maximum_colour_index: cgm_enums.IndexColour @dataclass class ColourValueExtent(CGMCommand): id = (1, 10) minimum: Optional[int] maximum: Optional[int] first_component: Optional[Tuple[float, float]] second_component: Optional[Tuple[float, float]] third_component: Optional[Tuple[float, float]] @classmethod def get_binary_args(cls, stream: BinaryCGMCommandParser, length): if stream.colour_model in ( cgm_enums.ColourModelEnum.CMYK, cgm_enums.ColourModelEnum.RGB, ): return stream.read_direct_colour(), stream.read_direct_colour(), 0, 0, 0 else: return 0, 0, stream.read_real(), stream.read_real(), stream.read_real() @dataclass class MetafileElementList(CGMCommand): id = (1, 11) elements: List[Tuple[int, int]] @classmethod def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int): num = stream.read_signed() yield [(stream.read_index(), stream.read_index()) for _ in range(num)] @dataclass class MetafileDefaultsReplacement(CGMCommand): id = (1, 12) data: List[CGMCommand] @classmethod def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int): data = stream.read(length) yield stream.parse_data(BytesIO(data)) @dataclass class FontList(CGMCommand): id = (1, 13) binary_params = ("SF",) fonts: List[str] @dataclass class CharacterSetList(CGMCommand): id = (1, 14) binary_params = (("E", "SF"),) specification: List[Tuple[cgm_enums.CharacterSetTypeEnum, str]] @dataclass class CharacterCodingAnnouncer(CGMCommand): id = (1, 15) binary_params = ("E",) encoding: cgm_enums.CharacterCodingEnum @dataclass class MaximumVDCExtent(CGMCommand): id = (1, 17) binary_params = "P", "P" first_corner: cgm_enums.Point second_corner: cgm_enums.Point @dataclass class ColourModel(ParserModifierCommand): id = (1, 19) binary_params = ("IX",) colour_model: cgm_enums.ColourModelEnum @dataclass class ScalingMode(CGMCommand): id = (2, 1) binary_params = "E", "F" scaling_mode: cgm_enums.ScalingModeEnum metric_scaling_factor: float @dataclass class ColourSelectionMode(ParserModifierCommand): id = (2, 2) binary_params = ("E",) colour_selection_mode: cgm_enums.ColourSelectionModeEnum @dataclass class LineWidthSpecificationMode(ParserModifierCommand): id = (2, 3) binary_params = ("E",) size_specification: cgm_enums.WidthSpecificationModeEnum @dataclass class MarkerSizeSpecificationMode(ParserModifierCommand): id = (2, 4) binary_params = ("E",) size_specification: cgm_enums.WidthSpecificationModeEnum @dataclass class EdgeWidthSpecificationMode(ParserModifierCommand): id = (2, 5) binary_params = ("E",) size_specification: cgm_enums.WidthSpecificationModeEnum @dataclass class VDCExtent(CGMCommand): id = (2, 6) binary_params = "P", "P" first_corner: cgm_enums.Point second_corner: cgm_enums.Point @dataclass class BackgroundColour(CGMCommand): id = (2, 7) binary_params = ("CD",) colour: cgm_enums.DirectColour @dataclass class InteriorStyleSpecificationMode(ParserModifierCommand): id = (2, 16) binary_params = ("E",) size_specification: cgm_enums.WidthSpecificationModeEnum @dataclass class VDCIntegerPrecision(ParserModifierCommand): id = (3, 1) binary_params = ("I",) integer_precision: int @dataclass class VDCRealPrecision(ParserModifierCommand): id = (3, 2) binary_params = "E", "I", "I" representation: cgm_enums.RealTypeEnum exponent_whole_size: int fraction_size: int @dataclass class Transparency(CGMCommand): id = (3, 4) binary_params = ("E",) indicator: cgm_enums.TransparencyEnum @dataclass class ClipRectangle(CGMCommand): id = (3, 5) binary_params = ("P", "P") p1: cgm_enums.Point p2: cgm_enums.Point @dataclass class ClipIndicator(CGMCommand): id = (3, 6) binary_params = ("E",) clip: cgm_enums.ClipIndicatorEnum @dataclass class Polyline(CGMCommand): id = (4, 1) binary_params = ("P",) points: List[cgm_enums.Point] @dataclass class DisjointPolyline(CGMCommand): id = (4, 2) binary_params = ("P",) points: List[cgm_enums.Point] @dataclass class Polymarker(CGMCommand): id = (4, 3) binary_params = ("P",) points: List[cgm_enums.Point] @dataclass class Text(CGMCommand): id = (4, 4) binary_params = ("P", "E", "S") point: cgm_enums.Point flag: cgm_enums.TextFinalFlag string: str @dataclass class RestrictedText(CGMCommand): id = (4, 5) binary_params = ("VDC", "VDC", "P", "E", "S") width: float height: float point: cgm_enums.Point flag: cgm_enums.TextFinalFlag string: str @dataclass class AppendText(CGMCommand): id = (4, 6) binary_params = ("E", "S") flag: cgm_enums.TextFinalFlag string: str @dataclass class Polygon(CGMCommand): id = (4, 7) binary_params = ("P",) points: List[cgm_enums.Point] @dataclass class PolygonSet(CGMCommand): id = (4, 8) points: List[cgm_enums.PolygonSetEntry] binary_params = (("P", "E"),) # TODO: Cell array (complicated parsing with sub-byte values) @dataclass class GeneralizedDrawingPrimitive(CGMCommand): id = (4, 10) identifier: int points: List[cgm_enums.Point] data: bytes @classmethod def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int): yield stream.read_signed() num = stream.read_signed() points = [stream.read_point() for _ in range(num)] yield points read = stream.i_size() * 2 + num * stream.p_size() yield stream.read(length - read) @dataclass class Rectangle(CGMCommand): id = (4, 11) binary_params = ("P", "P") p1: cgm_enums.Point p2: cgm_enums.Point @dataclass class Circle(CGMCommand): id = (4, 12) binary_params = ("P", "VDC") center: cgm_enums.Point radius: float @dataclass class CircularArc3Point(CGMCommand): id = (4, 13) binary_params = ("P", "P", "P") p1: cgm_enums.Point p2: cgm_enums.Point p3: cgm_enums.Point @dataclass class CircularArc3PointClose(CircularArc3Point): id = (4, 14) binary_params = ("P", "P", "P", "E") # type: ignore closure: cgm_enums.ArcClosureEnum @dataclass class CircularArcCentre(CGMCommand): id = (4, 15) binary_params = ("P", "VDC", "VDC", "VDC", "VDC", "VDC") # type: ignore centre: cgm_enums.Point delta_x_start: float delta_y_start: float delta_x_end: float delta_y_end: float radius: float @dataclass class CircularArcCentreClose(CircularArcCentre): id = (4, 16) binary_params = ("P", "VDC", "VDC", "VDC", "VDC", "VDC", "E") # type: ignore closure: cgm_enums.ArcClosureEnum @dataclass class Ellipse(CGMCommand): id = (4, 17) binary_params = ("P", "P", "P") center: cgm_enums.Point point_1: cgm_enums.Point point_2: cgm_enums.Point @dataclass class EllipticalArc(Ellipse): id = (4, 18) binary_params = ("P", "P", "P", "VDC", "VDC", "VDC", "VDC") # type: ignore delta_x_start: float delta_y_start: float delta_x_end: float delta_y_end: float @dataclass class EllipticalArcClose(EllipticalArc): id = (4, 19) binary_params = ("P", "P", "P", "VDC", "VDC", "VDC", "VDC", "E") # type: ignore closure: cgm_enums.ArcClosureEnum @dataclass class Polybezier(CGMCommand): id = (4, 26) binary_params = ("E", "P") continuous: cgm_enums.PolybezierContinuityEnum points: List[cgm_enums.Point] @dataclass class LineType(CGMCommand): id = (5, 2) binary_params = ("IX",) type: cgm_enums.LineTypeEnum @dataclass class LineWidth(CGMCommand): id = (5, 3) binary_params = ("LSS",) width: float @dataclass class LineColour(CGMCommand): id = (5, 4) binary_params = ("CO",) colour: cgm_enums.Colour @dataclass class MarkerType(CGMCommand): id = (5, 6) binary_params = ("E",) type: cgm_enums.MarkerTypeEnum @dataclass class MarkerSize(CGMCommand): id = (5, 7) binary_params = ("MSS",) size: float @dataclass class MarkerColour(CGMCommand): id = (5, 8) binary_params = ("CO",) colour: cgm_enums.Colour @dataclass class TextFontIndex(CGMCommand): id = (5, 10) binary_params = ("IX",) index: int @dataclass class TextPrecision(CGMCommand): id = (5, 11) binary_params = ("E",) text_precision: cgm_enums.TextPrecisionEnum @dataclass class CharacterExpansionFactor(CGMCommand): id = (5, 12) binary_params = ("R",) factor: float @dataclass class CharacterSpacing(CGMCommand): id = (5, 13) binary_params = ("R",) spacing: float @dataclass class TextColour(CGMCommand): id = (5, 14) binary_params = ("CO",) colour: cgm_enums.Colour @dataclass class CharacterHeight(CGMCommand): id = (5, 15) binary_params = ("VDC",) height: float @dataclass class CharacterOrientation(CGMCommand): id = (5, 16) binary_params = ("VDC", "VDC", "VDC", "VDC") x_character_up: float y_character_up: float x_character_base: float y_character_base: float @dataclass class TextPath(CGMCommand): id = (5, 17) binary_params = ("E",) path: cgm_enums.TextPathEnum @dataclass class TextAlignment(CGMCommand): id = (5, 18) binary_params = ("E", "E", "R", "R") horizontal_alignment: cgm_enums.TextHorizontalAlignmentEnum vertical_alignment: cgm_enums.TextVerticalAlignmentEnum continous_horizontal_alignment: float continous_vertical_alignment: float @dataclass class CharacterSetIndex(CGMCommand): id = (5, 19) binary_params = ("IX",) index: int @dataclass class AlternateCharacterSetIndex(CGMCommand): id = (5, 20) binary_params = ("IX",) index: int @dataclass class InteriorStyle(CGMCommand): id = (5, 22) binary_params = ("E",) interior_style: cgm_enums.InteriorStyleEnum @dataclass class FillColour(CGMCommand): id = (5, 23) binary_params = ("CO",) fill: cgm_enums.Colour @dataclass class HatchIndex(CGMCommand): id = (5, 24) binary_params = ("IX",) index: int @dataclass class PatternIndex(CGMCommand): id = (5, 25) binary_params = ("IX",) index: int @dataclass class EdgeType(CGMCommand): id = (5, 27) binary_params = ("IX",) type: cgm_enums.LineTypeEnum @dataclass class EdgeWidth(CGMCommand): id = (5, 28) binary_params = ("ESS",) width: float @dataclass class EdgeColour(CGMCommand): id = (5, 29) binary_params = ("CO",) colour: cgm_enums.Colour @dataclass class EdgeVisibility(CGMCommand): id = (5, 30) binary_params = ("E",) visibility: cgm_enums.EdgeVisibilityEnum @dataclass class ColourTable(CGMCommand): id = (5, 34) binary_params = ("CI", "CD") colour_index: cgm_enums.IndexColour colours: List[cgm_enums.DirectColour] @dataclass class AspectSourceFlags(CGMCommand): id = (5, 35) binary_params = (("E", "E"),) flags: List[Tuple[cgm_enums.ASFTypeEnum, cgm_enums.ASFValue]] @dataclass class LineCap(CGMCommand): id = (5, 37) binary_params = ("E", "E") cap: cgm_enums.CapIndicatorEnum dash_cap: cgm_enums.DashCapIndicatorEnum @dataclass class RestrictedTextType(CGMCommand): id = (5, 42) binary_params = ("E",) type: cgm_enums.RestrictionTypeEnum @dataclass class ApplicationData(CGMCommand): id = (7, 2) binary_params = ("I", "SF") identifier: int data: str