1# Copyright (c) 2011-2021, Manfred Moitzi 2# License: MIT License 3from typing import ( 4 TYPE_CHECKING, TextIO, BinaryIO, Iterable, Union, Tuple, Callable, 5 cast, Optional, List, 6) 7from datetime import datetime 8import io 9import base64 10import logging 11from itertools import chain 12 13from ezdxf.layouts import Modelspace 14from ezdxf.lldxf import const 15from ezdxf.lldxf.const import ( 16 BLK_XREF, BLK_EXTERNAL, DXF13, DXF14, DXF2000, DXF2007, DXF12, DXF2013, 17) 18from ezdxf.lldxf import loader 19from ezdxf.lldxf.tagwriter import TagWriter, BinaryTagWriter 20 21from ezdxf.entitydb import EntityDB 22from ezdxf.layouts.layouts import Layouts 23from ezdxf.tools.codepage import tocodepage, toencoding 24from ezdxf.tools.juliandate import juliandate 25from ezdxf.options import options 26 27from ezdxf.tools import guid 28from ezdxf.query import EntityQuery 29from ezdxf.groupby import groupby 30from ezdxf.render.dimension import DimensionRenderer 31 32from ezdxf.sections.header import HeaderSection 33from ezdxf.sections.classes import ClassesSection 34from ezdxf.sections.tables import TablesSection 35from ezdxf.sections.blocks import BlocksSection 36from ezdxf.sections.entities import EntitySection, StoredSection 37from ezdxf.sections.objects import ObjectsSection 38from ezdxf.sections.acdsdata import AcDsDataSection 39 40from ezdxf.entities.dxfgroups import GroupCollection 41from ezdxf.entities.material import MaterialCollection 42from ezdxf.entities.mleader import MLeaderStyleCollection 43from ezdxf.entities.mline import MLineStyleCollection 44 45logger = logging.getLogger('ezdxf') 46 47if TYPE_CHECKING: 48 from ezdxf.eztypes import ( 49 DXFTag, Table, ViewportTable, VPort, Dictionary, Layout, 50 DXFEntity, Layer, Auditor, GenericLayoutType, 51 ) 52 from pathlib import Path 53 54FIXED_GUID = '{00000000-0000-0000-0000-000000000000}' 55 56def _validate_handle_seed(seed: str) -> str: 57 from ezdxf.tools.handle import START_HANDLE 58 if seed is None: 59 seed = START_HANDLE 60 try: 61 v = int(seed, 16) 62 if v < 1: 63 seed = START_HANDLE 64 except ValueError: 65 seed = START_HANDLE 66 return seed 67 68 69class Drawing: 70 def __init__(self, dxfversion=DXF2013): 71 self.entitydb = EntityDB() 72 target_dxfversion = dxfversion.upper() 73 self._dxfversion: str = const.acad_release_to_dxf_version.get( 74 target_dxfversion, target_dxfversion) 75 if self._dxfversion not in const.versions_supported_by_new: 76 raise const.DXFVersionError( 77 f'Unsupported DXF version "{self.dxfversion}".') 78 # Store original dxf version if loaded (and maybe converted R13/14) 79 # from file. 80 self._loaded_dxfversion: Optional[str] = None 81 82 # Status flag which is True while loading content from a DXF file: 83 self.is_loading = False 84 self.encoding: str = 'cp1252' # read/write 85 self.filename: Optional[str] = None 86 87 # named objects dictionary 88 self.rootdict: 'Dictionary' = None 89 90 # DXF sections 91 self.header: HeaderSection = None 92 self.classes: ClassesSection = None 93 self.tables: TablesSection = None 94 self.blocks: BlocksSection = None 95 self.entities: EntitySection = None 96 self.objects: ObjectsSection = None 97 98 # DXF R2013 and later 99 self.acdsdata: AcDsDataSection = None 100 101 self.stored_sections = [] 102 self.layouts: Layouts = None 103 self.groups: GroupCollection = None 104 self.materials: MaterialCollection = None 105 self.mleader_styles: MLeaderStyleCollection = None 106 self.mline_styles: MLineStyleCollection = None 107 108 # Set to False if the generated DXF file will be incompatible to AutoCAD 109 self._acad_compatible = True 110 # Store reasons for AutoCAD incompatibility: 111 self._acad_incompatibility_reason = set() 112 113 # DIMENSION rendering engine can be replaced by a custom Dimension 114 # render: see property Drawing.dimension_renderer 115 self._dimension_renderer = DimensionRenderer() 116 117 # Some fixes can't be applied while the DXF document is not fully 118 # initialized, store this fixes as callable object: 119 self._post_init_commands: List[Callable] = [] 120 # Don't create any new entities here: 121 # New created handles could collide with handles loaded from DXF file. 122 assert len(self.entitydb) == 0 123 124 @classmethod 125 def new(cls, dxfversion: str = DXF2013) -> 'Drawing': 126 """ Create new drawing. Package users should use the factory function 127 :func:`ezdxf.new`. (internal API) 128 """ 129 doc = cls(dxfversion) 130 doc._setup() 131 return doc 132 133 def _setup(self): 134 self.header = HeaderSection.new() 135 self.classes = ClassesSection(self) 136 self.tables = TablesSection(self) 137 self.blocks = BlocksSection(self) 138 self.entities = EntitySection(self) 139 self.objects = ObjectsSection(self) 140 # AcDSData section is not supported for new drawings 141 self.acdsdata = AcDsDataSection(self) 142 self.rootdict = self.objects.rootdict 143 # Create missing tables: 144 self.objects.setup_objects_management_tables(self.rootdict) 145 self.layouts = Layouts.setup(self) 146 self._finalize_setup() 147 148 def _finalize_setup(self): 149 """ Common setup tasks for new and loaded DXF drawings. """ 150 self.groups = GroupCollection(self) 151 self.materials = MaterialCollection(self) 152 153 self.mline_styles = MLineStyleCollection(self) 154 # all required internal structures are ready 155 # now do the stuff to please AutoCAD 156 self._create_required_table_entries() 157 158 # mleader_styles requires text styles 159 self.mleader_styles = MLeaderStyleCollection(self) 160 self._set_required_layer_attributes() 161 self._setup_metadata() 162 self._execute_post_init_commands() 163 164 def _execute_post_init_commands(self): 165 for cmd in self._post_init_commands: 166 cmd() 167 del self._post_init_commands 168 169 def _create_required_table_entries(self): 170 self._create_required_vports() 171 self._create_required_linetypes() 172 self._create_required_layers() 173 self._create_required_styles() 174 self._create_required_appids() 175 self._create_required_dimstyles() 176 177 def _set_required_layer_attributes(self): 178 for layer in self.layers: # type: Layer 179 layer.set_required_attributes() 180 181 def _create_required_vports(self): 182 if '*Active' not in self.viewports: 183 self.viewports.new('*Active') 184 185 def _create_required_appids(self): 186 if 'ACAD' not in self.appids: 187 self.appids.new('ACAD') 188 189 def _create_required_linetypes(self): 190 linetypes = self.linetypes 191 for name in ('ByBlock', 'ByLayer', 'Continuous'): 192 if name not in linetypes: 193 linetypes.new(name) 194 195 def _create_required_dimstyles(self): 196 if 'Standard' not in self.dimstyles: 197 self.dimstyles.new('Standard') 198 199 def _create_required_styles(self): 200 if 'Standard' not in self.styles: 201 self.styles.new('Standard') 202 203 def _create_required_layers(self): 204 layers = self.layers 205 if '0' not in layers: 206 layers.new('0') 207 if 'Defpoints' not in layers: 208 layers.new('Defpoints', dxfattribs={'plot': 0}) # do not plot 209 else: 210 # AutoCAD requires a plot flag = 0 211 layers.get('Defpoints').dxf.plot = 0 212 213 def _setup_metadata(self): 214 self.header['$ACADVER'] = self.dxfversion 215 self.header['$TDCREATE'] = juliandate(datetime.now()) 216 self.reset_fingerprint_guid() 217 self.reset_version_guid() 218 219 @property 220 def dxfversion(self) -> str: 221 """ Get current DXF version. """ 222 return self._dxfversion 223 224 @dxfversion.setter 225 def dxfversion(self, version) -> None: 226 """ Set current DXF version. """ 227 self._dxfversion = self._validate_dxf_version(version) 228 self.header['$ACADVER'] = version 229 230 @property 231 def output_encoding(self): 232 """ Returns required output encoding for writing document to a text 233 streams. 234 """ 235 return 'utf-8' if self.dxfversion >= DXF2007 else self.encoding 236 237 @property 238 def units(self) -> int: 239 """ Get and set the document/modelspace base units as enum, for more 240 information read this: :ref:`dxf units`. 241 242 """ 243 return self.header.get('$INSUNITS', 0) 244 245 @units.setter 246 def units(self, unit_enum: int) -> None: 247 if 0 <= unit_enum < 25: 248 self.header['$INSUNITS'] = unit_enum 249 else: 250 raise ValueError(f'Invalid units enum: {unit_enum}') 251 252 def _validate_dxf_version(self, version: str) -> str: 253 version = version.upper() 254 # translates 'R12' -> 'AC1009' 255 version = const.acad_release_to_dxf_version.get(version, version) 256 if version not in const.versions_supported_by_save: 257 raise const.DXFVersionError(f'Unsupported DXF version "{version}".') 258 if version == DXF12: 259 if self._dxfversion > DXF12: 260 logger.warning( 261 f'Downgrade from DXF {self.acad_release} to R12 may create ' 262 f'an invalid DXF file.') 263 elif version < self._dxfversion: 264 logger.info( 265 f'Downgrade from DXF {self.acad_release} to ' 266 f'{const.acad_release[version]} can cause lost of features.') 267 return version 268 269 @classmethod 270 def read(cls, stream: TextIO) -> 'Drawing': 271 """ Open an existing drawing. Package users should use the factory 272 function :func:`ezdxf.read`. To preserve possible binary data in 273 XRECORD entities use :code:`errors='surrogateescape'` as error handler 274 for the import stream. 275 276 Args: 277 stream: text stream yielding text (unicode) strings by readline() 278 279 """ 280 from .lldxf.tagger import ascii_tags_loader 281 tag_loader = ascii_tags_loader(stream) 282 return cls.load(tag_loader) 283 284 @classmethod 285 def load(cls, tag_loader: Iterable['DXFTag']) -> 'Drawing': 286 """ Load DXF document from a DXF tag loader, in general an external 287 untrusted source. 288 289 Args: 290 tag_loader: DXF tag loader 291 292 """ 293 from .lldxf.tagger import tag_compiler 294 tag_loader = tag_compiler(tag_loader) 295 doc = cls() 296 doc._load(tag_loader) 297 return doc 298 299 @classmethod 300 def from_tags(cls, compiled_tags: Iterable['DXFTag']) -> 'Drawing': 301 """ Create new drawing from compiled tags. (internal API)""" 302 doc = cls() 303 doc._load(tagger=compiled_tags) 304 return doc 305 306 def _load(self, tagger: Optional[Iterable['DXFTag']]) -> None: 307 # 1st Loading stage: load complete DXF entity structure 308 self.is_loading = True 309 sections = loader.load_dxf_structure(tagger) 310 if 'THUMBNAILIMAGE' in sections: 311 del sections['THUMBNAILIMAGE'] 312 self._load_section_dict(sections) 313 314 def _load_section_dict(self, sections: loader.SectionDict) -> None: 315 """ Internal API to load a DXF document from a section dict. """ 316 self.is_loading = True 317 # Create header section: 318 # All header tags are the first DXF structure entity 319 header_entities = sections.get('HEADER', [None])[0] 320 if header_entities is None: 321 # Create default header, files without header are by default DXF R12 322 self.header = HeaderSection.new(dxfversion=DXF12) 323 else: 324 self.header = HeaderSection.load(header_entities) 325 326 self._dxfversion: str = self.header.get('$ACADVER', DXF12) 327 328 # Store original DXF version of loaded file. 329 self._loaded_dxfversion = self._dxfversion 330 331 # Content encoding: 332 self.encoding = toencoding(self.header.get('$DWGCODEPAGE', 'ANSI_1252')) 333 334 # Set handle seed: 335 seed: str = self.header.get('$HANDSEED', str(self.entitydb.handles)) 336 self.entitydb.handles.reset(_validate_handle_seed(seed)) 337 338 # Store all necessary DXF entities in the entity database: 339 loader.load_and_bind_dxf_content(sections, self) 340 341 # End of 1. loading stage, all entities of the DXF file are 342 # stored in the entity database. 343 344 # Create sections: 345 self.classes = ClassesSection(self, sections.get('CLASSES', None)) 346 self.tables = TablesSection(self, sections.get('TABLES', None)) 347 348 # Create *Model_Space and *Paper_Space BLOCK_RECORDS 349 # BlockSection setup takes care about the rest: 350 self._create_required_block_records() 351 352 # At this point all table entries are required: 353 self.blocks = BlocksSection(self, sections.get('BLOCKS', None)) 354 self.entities = EntitySection(self, sections.get('ENTITIES', None)) 355 self.objects = ObjectsSection(self, sections.get('OBJECTS', None)) 356 357 # only DXF R2013+ 358 self.acdsdata = AcDsDataSection(self, sections.get('ACDSDATA', None)) 359 360 # Store unmanaged sections as raw tags: 361 for name, data in sections.items(): 362 if name not in const.MANAGED_SECTIONS: 363 self.stored_sections.append(StoredSection(data)) 364 365 # Objects section is not initialized! 366 self._2nd_loading_stage() 367 368 # DXF version upgrades: 369 if self.dxfversion < DXF12: 370 logger.info('DXF version upgrade to DXF R12.') 371 self.dxfversion = DXF12 372 373 if self.dxfversion == DXF12: 374 self.tables.create_table_handles() 375 376 if self.dxfversion in (DXF13, DXF14): 377 logger.info('DXF version upgrade to DXF R2000.') 378 self.dxfversion = DXF2000 379 self.create_all_arrow_blocks() 380 381 # Objects section setup: 382 self.rootdict = self.objects.rootdict 383 # Create missing management tables (DICTIONARY): 384 self.objects.setup_objects_management_tables(self.rootdict) 385 386 # Setup modelspace- and paperspace layouts: 387 self.layouts = Layouts.load(self) 388 389 # Additional work is common to the new and load process: 390 self.is_loading = False 391 self._finalize_setup() 392 393 def _2nd_loading_stage(self): 394 """ Load additional resources from entity database into DXF entities. 395 396 e.g. convert handles into DXFEntity() objects 397 398 """ 399 db = self.entitydb 400 for entity in db.values(): 401 # The post_load_hook() can return a callable, which should be 402 # executed, when the DXF document is fully initialized. 403 cmd = entity.post_load_hook(self) 404 if cmd is not None: 405 self._post_init_commands.append(cmd) 406 407 def create_all_arrow_blocks(self): 408 """ For upgrading DXF R12/13/14 files to R2000, it is necessary to 409 create all used arrow blocks before saving the DXF file, else $HANDSEED 410 is not the next available handle, which is a problem for AutoCAD. 411 To be save create all known AutoCAD arrows, because references to arrow 412 blocks can be in DIMSTYLE, DIMENSION override, LEADER override and maybe 413 other places. 414 415 """ 416 from ezdxf.render.arrows import ARROWS 417 for arrow_name in ARROWS.__acad__: 418 ARROWS.create_block(self.blocks, arrow_name) 419 420 def _create_required_block_records(self): 421 if '*Model_Space' not in self.block_records: 422 self.block_records.new('*Model_Space') 423 if '*Paper_Space' not in self.block_records: 424 self.block_records.new('*Paper_Space') 425 426 def saveas(self, filename: Union[str, 'Path'], encoding: str = None, 427 fmt: str = 'asc') -> None: 428 """ Set :class:`Drawing` attribute :attr:`filename` to `filename` and 429 write drawing to the file system. Override file encoding by argument 430 `encoding`, handle with care, but this option allows you to create DXF 431 files for applications that handles file encoding different than 432 AutoCAD. 433 434 Args: 435 filename: file name as string 436 encoding: override default encoding as Python encoding string like ``'utf-8'`` 437 fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for Binary DXF 438 439 """ 440 self.filename = str(filename) 441 self.save(encoding=encoding, fmt=fmt) 442 443 def save(self, encoding: str = None, fmt: str = 'asc') -> None: 444 """ Write drawing to file-system by using the :attr:`filename` attribute 445 as filename. Override file encoding by argument `encoding`, handle with 446 care, but this option allows you to create DXF files for applications 447 that handles file encoding different than AutoCAD. 448 449 Args: 450 encoding: override default encoding as Python encoding string like ``'utf-8'`` 451 fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for Binary DXF 452 453 """ 454 # DXF R12, R2000, R2004 - ASCII encoding 455 # DXF R2007 and newer - UTF-8 encoding 456 # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode 457 # characters. 458 459 if encoding is None: 460 enc = self.output_encoding 461 else: 462 # override default encoding, for applications that handle encoding 463 # different than AutoCAD 464 enc = encoding 465 466 if fmt.startswith('asc'): 467 fp = io.open(self.filename, mode='wt', encoding=enc, 468 errors='dxfreplace') 469 elif fmt.startswith('bin'): 470 fp = open(self.filename, 'wb') 471 else: 472 raise ValueError(f"Unknown output format: '{fmt}'.") 473 try: 474 self.write(fp, fmt=fmt) 475 finally: 476 fp.close() 477 478 def encode(self, s: str) -> bytes: 479 """ Encode string `s` with correct encoding and error handler. """ 480 return s.encode(encoding=self.output_encoding, errors='dxfreplace') 481 482 def write(self, stream: Union[TextIO, BinaryIO], fmt: str = 'asc') -> None: 483 """ Write drawing as ASCII DXF to a text stream or as Binary DXF to a 484 binary stream. For DXF R2004 (AC1018) and prior open stream with 485 drawing :attr:`encoding` and :code:`mode='wt'`. For DXF R2007 (AC1021) 486 and later use :code:`encoding='utf-8'`, or better use the later added 487 :class:`Drawing` property :attr:`output_encoding` which returns the 488 correct encoding automatically. The correct and required error handler 489 is :code:`errors='dxfreplace'`! 490 491 If writing to a :class:`StringIO` stream, use :meth:`Drawing.encode` to 492 encode the result string from :meth:`StringIO.get_value`:: 493 494 binary = doc.encode(stream.get_value()) 495 496 Args: 497 stream: output text stream or binary stream 498 fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for binary DXF 499 500 """ 501 dxfversion = self.dxfversion 502 if dxfversion == DXF12: 503 handles = bool(self.header.get('$HANDLING', 0)) 504 else: 505 handles = True 506 if dxfversion > DXF12: 507 self.classes.add_required_classes(dxfversion) 508 509 self._create_appids() 510 self._update_header_vars() 511 self.update_extents() 512 self.update_limits() 513 self._update_metadata() 514 515 if fmt.startswith('asc'): 516 tagwriter = TagWriter(stream, write_handles=handles, 517 dxfversion=dxfversion) 518 elif fmt.startswith('bin'): 519 tagwriter = BinaryTagWriter( 520 stream, write_handles=handles, dxfversion=dxfversion, 521 encoding=self.output_encoding, 522 ) 523 tagwriter.write_signature() 524 else: 525 raise ValueError(f"Unknown output format: '{fmt}'.") 526 527 self.export_sections(tagwriter) 528 529 def encode_base64(self) -> bytes: 530 """ Returns DXF document as base64 encoded binary data. """ 531 stream = io.StringIO() 532 self.write(stream) 533 # Create binary data: 534 binary_data = self.encode(stream.getvalue()) 535 # Create Windows line endings and do base64 encoding: 536 return base64.encodebytes(binary_data.replace(b'\n', b'\r\n')) 537 538 def export_sections(self, tagwriter: 'TagWriter') -> None: 539 """ DXF export sections. (internal API) """ 540 dxfversion = tagwriter.dxfversion 541 self.header.export_dxf(tagwriter) 542 if dxfversion > DXF12: 543 self.classes.export_dxf(tagwriter) 544 self.tables.export_dxf(tagwriter) 545 self.blocks.export_dxf(tagwriter) 546 self.entities.export_dxf(tagwriter) 547 if dxfversion > DXF12: 548 self.objects.export_dxf(tagwriter) 549 if self.acdsdata.is_valid: 550 self.acdsdata.export_dxf(tagwriter) 551 for section in self.stored_sections: 552 section.export_dxf(tagwriter) 553 554 tagwriter.write_tag2(0, 'EOF') 555 556 def update_extents(self): 557 msp = self.modelspace() 558 self.header['$EXTMIN'] = msp.dxf.extmin 559 self.header['$EXTMAX'] = msp.dxf.extmax 560 active_layout = self.active_layout() 561 self.header['$PEXTMIN'] = active_layout.dxf.extmin 562 self.header['$PEXTMAX'] = active_layout.dxf.extmax 563 564 def update_limits(self): 565 msp = self.modelspace() 566 self.header['$LIMMIN'] = msp.dxf.limmin 567 self.header['$LIMMAX'] = msp.dxf.limmax 568 active_layout = self.active_layout() 569 self.header['$PLIMMIN'] = active_layout.dxf.limmin 570 self.header['$PLIMMAX'] = active_layout.dxf.limmax 571 572 def _update_header_vars(self): 573 from ezdxf.lldxf.const import acad_maint_ver 574 575 # set or correct $CMATERIAL handle 576 material = self.entitydb.get(self.header.get('$CMATERIAL', None)) 577 if material is None or material.dxftype() != 'MATERIAL': 578 if 'ByLayer' in self.materials: 579 self.header['$CMATERIAL'] = self.materials.get( 580 'ByLayer').dxf.handle 581 else: # set any handle, except '0' which crashes BricsCAD 582 self.header['$CMATERIAL'] = '45' 583 584 # set ACAD maintenance version - same values as used by BricsCAD 585 self.header['$ACADMAINTVER'] = acad_maint_ver.get(self.dxfversion, 0) 586 587 def _update_metadata(self): 588 if options.write_fixed_meta_data_for_testing: 589 fixed_date = juliandate(datetime(2000, 1, 1, 0, 0)) 590 self.header['$TDCREATE'] = fixed_date 591 self.header['$TDUCREATE'] = fixed_date 592 self.header['$TDUPDATE'] = fixed_date 593 self.header['$TDUUPDATE'] = fixed_date 594 self.header['$VERSIONGUID'] = FIXED_GUID 595 self.header['$FINGERPRINTGUID'] = FIXED_GUID 596 else: 597 now = datetime.now() 598 self.header['$TDUPDATE'] = juliandate(now) 599 self.reset_version_guid() 600 601 self.header['$HANDSEED'] = str(self.entitydb.handles) # next handle 602 self.header['$DWGCODEPAGE'] = tocodepage(self.encoding) 603 604 def _create_appid_if_not_exist(self, name: str, flags: int = 0) -> None: 605 if name not in self.appids: 606 self.appids.new(name, {'flags': flags}) 607 608 def _create_appids(self): 609 self._create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0) 610 611 @property 612 def acad_release(self) -> str: 613 """ Returns the AutoCAD release number like ``'R12'`` or ``'R2000'``. 614 """ 615 return const.acad_release.get(self.dxfversion, "unknown") 616 617 @property 618 def layers(self) -> 'Table': 619 return self.tables.layers 620 621 @property 622 def linetypes(self) -> 'Table': 623 return self.tables.linetypes 624 625 @property 626 def styles(self) -> 'Table': 627 return self.tables.styles 628 629 @property 630 def dimstyles(self) -> 'Table': 631 return self.tables.dimstyles 632 633 @property 634 def ucs(self) -> 'Table': 635 return self.tables.ucs 636 637 @property 638 def appids(self) -> 'Table': 639 return self.tables.appids 640 641 @property 642 def views(self) -> 'Table': 643 return self.tables.views 644 645 @property 646 def block_records(self) -> 'Table': 647 return self.tables.block_records 648 649 @property 650 def viewports(self) -> 'ViewportTable': 651 return self.tables.viewports 652 653 @property 654 def plotstyles(self) -> 'Dictionary': 655 return self.rootdict['ACAD_PLOTSTYLENAME'] 656 657 @property 658 def dimension_renderer(self) -> DimensionRenderer: 659 return self._dimension_renderer 660 661 @dimension_renderer.setter 662 def dimension_renderer(self, renderer: DimensionRenderer) -> None: 663 """ 664 Set your own dimension line renderer if needed. 665 666 see also: ezdxf.render.dimension 667 668 """ 669 self._dimension_renderer = renderer 670 671 def modelspace(self) -> 'Modelspace': 672 """ Returns the modelspace layout, displayed as ``'Model'`` tab in CAD 673 applications, defined by block record named ``'*Model_Space'``. 674 """ 675 return self.layouts.modelspace() 676 677 def layout(self, name: str = None) -> 'Layout': 678 """ Returns paperspace layout `name` or returns first layout in tab 679 order if `name` is ``None``. 680 """ 681 return self.layouts.get(name) 682 683 def active_layout(self) -> 'Layout': 684 """ Returns the active paperspace layout, defined by block record 685 name ``'*Paper_Space'``. 686 """ 687 return self.layouts.active_layout() 688 689 def layout_names(self) -> Iterable[str]: 690 """ Returns all layout names (modelspace ``'Model'`` included) in 691 arbitrary order. 692 """ 693 return list(self.layouts.names()) 694 695 def layout_names_in_taborder(self) -> Iterable[str]: 696 """ Returns all layout names (modelspace included, always first name) 697 in tab order. 698 """ 699 return list(self.layouts.names_in_taborder()) 700 701 def reset_fingerprint_guid(self): 702 """ Reset fingerprint GUID. """ 703 self.header['$FINGERPRINTGUID'] = guid() 704 705 def reset_version_guid(self): 706 """ Reset version GUID. """ 707 self.header['$VERSIONGUID'] = guid() 708 709 @property 710 def acad_compatible(self) -> bool: 711 """ Returns ``True`` if drawing is AutoCAD compatible. """ 712 return self._acad_compatible 713 714 def add_acad_incompatibility_message(self, msg: str): 715 """ Add AutoCAD incompatibility message. (internal API) """ 716 self._acad_compatible = False 717 if msg not in self._acad_incompatibility_reason: 718 self._acad_incompatibility_reason.add(msg) 719 logger.warning( 720 f'Drawing is incompatible to AutoCAD, because {msg}.') 721 722 def query(self, query: str = '*') -> EntityQuery: 723 """ 724 Entity query over all layouts and blocks, excluding the OBJECTS section. 725 726 Args: 727 query: query string 728 729 .. seealso:: 730 731 :ref:`entity query string` and :ref:`entity queries` 732 733 """ 734 return EntityQuery(self.chain_layouts_and_blocks(), query) 735 736 def groupby(self, dxfattrib="", key=None) -> dict: 737 """ Groups DXF entities of all layouts and blocks (excluding the 738 OBJECTS section) by a DXF attribute or a key function. 739 740 Args: 741 dxfattrib: grouping DXF attribute like ``'layer'`` 742 key: key function, which accepts a :class:`DXFEntity` as argument 743 and returns a hashable grouping key or ``None`` to ignore 744 this entity. 745 746 .. seealso:: 747 748 :func:`~ezdxf.groupby.groupby` documentation 749 750 """ 751 return groupby(self.chain_layouts_and_blocks(), dxfattrib, key) 752 753 def chain_layouts_and_blocks(self) -> Iterable['DXFEntity']: 754 """ Chain entity spaces of all layouts and blocks. Yields an iterator 755 for all entities in all layouts and blocks. 756 757 """ 758 layouts = list(self.layouts_and_blocks()) 759 return chain.from_iterable(layouts) 760 761 def layouts_and_blocks(self) -> Iterable['GenericLayoutType']: 762 """ Iterate over all layouts (modelspace and paperspace) and all 763 block definitions. 764 765 """ 766 return iter(self.blocks) 767 768 def delete_layout(self, name: str) -> None: 769 """ 770 Delete paper space layout `name` and all entities owned by this layout. 771 Available only for DXF R2000 or later, DXF R12 supports only one 772 paperspace and it can't be deleted. 773 774 """ 775 if name not in self.layouts: 776 raise const.DXFValueError(f"Layout '{name}' does not exist.") 777 else: 778 self.layouts.delete(name) 779 780 def new_layout(self, name, dxfattribs=None) -> 'Layout': 781 """ 782 Create a new paperspace layout `name`. Returns a 783 :class:`~ezdxf.layouts.Layout` object. 784 DXF R12 (AC1009) supports only one paperspace layout, only the active 785 paperspace layout is saved, other layouts are dismissed. 786 787 Args: 788 name: unique layout name 789 dxfattribs: additional DXF attributes for the 790 :class:`~ezdxf.entities.layout.DXFLayout` entity 791 792 Raises: 793 DXFValueError: :class:`~ezdxf.layouts.Layout` `name` already exist 794 795 """ 796 if name in self.layouts: 797 raise const.DXFValueError(f"Layout '{name}' already exists.") 798 else: 799 return self.layouts.new(name, dxfattribs) 800 801 def acquire_arrow(self, name: str): 802 """ For standard AutoCAD and ezdxf arrows create block definitions if 803 required, otherwise check if block `name` exist. (internal API) 804 805 """ 806 from ezdxf.render.arrows import ARROWS 807 if ARROWS.is_acad_arrow(name) or ARROWS.is_ezdxf_arrow(name): 808 ARROWS.create_block(self.blocks, name) 809 elif name not in self.blocks: 810 raise const.DXFValueError(f'Arrow block "{name}" does not exist.') 811 812 def add_image_def(self, filename: str, size_in_pixel: Tuple[int, int], 813 name=None): 814 """ Add an image definition to the objects section. 815 816 Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing 817 (objects section). `filename` is the image file name as relative or 818 absolute path and `size_in_pixel` is the image size in pixel as (x, y) 819 tuple. To avoid dependencies to external packages, `ezdxf` can not 820 determine the image size by itself. Returns a 821 :class:`~ezdxf.entities.image.ImageDef` entity which is needed to 822 create an image reference. `name` is the internal image name, if set to 823 ``None``, name is auto-generated. 824 825 Absolute image paths works best for AutoCAD but not really good, you 826 have to update external references manually in AutoCAD, which is not 827 possible in TrueView. If the drawing units differ from 1 meter, you 828 also have to use: :meth:`set_raster_variables`. 829 830 Args: 831 filename: image file name (absolute path works best for AutoCAD) 832 size_in_pixel: image size in pixel as (x, y) tuple 833 name: image name for internal use, None for using filename as name 834 (best for AutoCAD) 835 836 .. seealso:: 837 838 :ref:`tut_image` 839 840 """ 841 if 'ACAD_IMAGE_VARS' not in self.rootdict: 842 self.objects.set_raster_variables(frame=0, quality=1, units='m') 843 if name is None: 844 name = filename 845 return self.objects.add_image_def(filename, size_in_pixel, name) 846 847 def set_raster_variables(self, frame: int = 0, quality: int = 1, 848 units: str = 'm'): 849 """ 850 Set raster variables. 851 852 Args: 853 frame: ``0`` = do not show image frame; ``1`` = show image frame 854 quality: ``0`` = draft; ``1`` = high 855 units: units for inserting images. This defines the real world unit 856 for one drawing unit for the purpose of inserting and scaling 857 images with an associated resolution. 858 859 ===== =========================== 860 mm Millimeter 861 cm Centimeter 862 m Meter (ezdxf default) 863 km Kilometer 864 in Inch 865 ft Foot 866 yd Yard 867 mi Mile 868 ===== =========================== 869 870 """ 871 self.objects.set_raster_variables(frame=frame, quality=quality, 872 units=units) 873 874 def set_wipeout_variables(self, frame=0): 875 """ 876 Set wipeout variables. 877 878 Args: 879 frame: ``0`` = do not show image frame; ``1`` = show image frame 880 881 """ 882 self.objects.set_wipeout_variables(frame=frame) 883 var_dict = self.rootdict.get_required_dict('AcDbVariableDictionary') 884 var_dict.set_or_add_dict_var('WIPEOUTFRAME', str(frame)) 885 886 def add_underlay_def(self, filename: str, format: str = 'ext', 887 name: str = None): 888 """ Add an :class:`~ezdxf.entities.underlay.UnderlayDef` entity to the 889 drawing (OBJECTS section). 890 `filename` is the underlay file name as relative or absolute path and 891 `format` as string (pdf, dwf, dgn). 892 The underlay definition is required to create an underlay reference. 893 894 Args: 895 filename: underlay file name 896 format: file format as string ``'pdf'|'dwf'|'dgn'`` or ``'ext'`` 897 for getting file format from filename extension 898 name: pdf format = page number to display; dgn format = 899 ``'default'``; dwf: ???? 900 901 .. seealso:: 902 903 :ref:`tut_underlay` 904 905 """ 906 if format == 'ext': 907 format = filename[-3:] 908 return self.objects.add_underlay_def(filename, format, name) 909 910 def add_xref_def(self, filename: str, name: str, 911 flags: int = BLK_XREF | BLK_EXTERNAL): 912 """ 913 Add an external reference (xref) definition to the blocks section. 914 915 Args: 916 filename: external reference filename 917 name: name of the xref block 918 flags: block flags 919 920 """ 921 self.blocks.new(name=name, dxfattribs={ 922 'flags': flags, 923 'xref_path': filename 924 }) 925 926 def audit(self) -> 'Auditor': 927 """ Checks document integrity and fixes all fixable problems, not 928 fixable problems are stored in :attr:`Auditor.errors`. 929 930 If you are messing around with internal structures, call this method 931 before saving to be sure to export valid DXF documents, but be aware 932 this is a long running task. 933 934 """ 935 from ezdxf.audit import Auditor 936 auditor = Auditor(self) 937 auditor.run() 938 return auditor 939 940 def validate(self, print_report=True) -> bool: 941 """ Simple way to run an audit process. Fixes all fixable problems, 942 return ``False`` if not fixable errors occurs, to get more information 943 about not fixable errors use :meth:`audit` method instead. 944 945 Args: 946 print_report: print report to stdout 947 948 Returns: ``True`` if no errors occurred 949 950 """ 951 auditor = self.audit() 952 if len(auditor): 953 if print_report: 954 auditor.print_error_report() 955 return False 956 else: 957 return True 958 959 def set_modelspace_vport(self, height, center=(0, 0)) -> 'VPort': 960 r""" Set initial view/zoom location for the modelspace, this replaces 961 the current "\*Active" viewport configuration. 962 963 Args: 964 height: modelspace area to view 965 center: modelspace location to view in the center of the CAD 966 application window. 967 968 """ 969 self.viewports.delete_config('*Active') 970 vport = cast('VPort', self.viewports.new('*Active')) 971 vport.dxf.center = center 972 vport.dxf.height = height 973 return vport 974