1# Created: 13.03.2011 2# Copyright (c) 2011-2019, Manfred Moitzi 3# License: MIT License 4from typing import TYPE_CHECKING, Iterable, Tuple, cast, Iterator, Union 5import logging 6 7from ezdxf.entities.dictionary import Dictionary 8from ezdxf.entities import factory 9from ezdxf.lldxf.const import DXFStructureError, DXFValueError, RASTER_UNITS, DXFKeyError 10from ezdxf.entitydb import EntitySpace 11from ezdxf.query import EntityQuery 12from ezdxf.tools.handle import UnderlayKeyGenerator 13 14if TYPE_CHECKING: 15 from ezdxf.eztypes import GeoData, DictionaryVar 16 from ezdxf.eztypes import Drawing, TagWriter, EntityDB, DXFTagStorage, DXFObject 17 from ezdxf.eztypes import ImageDefReactor, ImageDef, UnderlayDef, DictionaryWithDefault, XRecord, Placeholder 18 19logger = logging.getLogger('ezdxf') 20 21 22class ObjectsSection: 23 def __init__(self, doc: 'Drawing', entities: Iterable['DXFObject'] = None): 24 self.doc = doc 25 self.underlay_key_generator = UnderlayKeyGenerator() 26 self._entity_space = EntitySpace() 27 if entities is not None: 28 self._build(iter(entities)) 29 30 @property 31 def entitydb(self) -> 'EntityDB': 32 """ Returns drawing entity database. (internal API)""" 33 return self.doc.entitydb 34 35 def get_entity_space(self) -> 'EntitySpace': 36 """ Returns entity space. (internal API) """ 37 return self._entity_space 38 39 def next_underlay_key(self, checkfunc=lambda k: True) -> str: 40 while True: 41 key = self.underlay_key_generator.next() 42 if checkfunc(key): 43 return key 44 45 def _build(self, entities: Iterator['DXFObject']) -> None: 46 section_head = next(entities) # type: DXFTagStorage 47 48 if section_head.dxftype() != 'SECTION' or section_head.base_class[1] != (2, 'OBJECTS'): 49 raise DXFStructureError("Critical structure error in 'OBJECTS' section.") 50 51 for entity in entities: 52 self._entity_space.add(entity) 53 54 def export_dxf(self, tagwriter: 'TagWriter') -> None: 55 """ Export DXF entity by `tagwriter`. (internal API) """ 56 tagwriter.write_str(" 0\nSECTION\n 2\nOBJECTS\n") 57 self._entity_space.export_dxf(tagwriter) 58 tagwriter.write_tag2(0, "ENDSEC") 59 60 def new_entity(self, _type: str, dxfattribs: dict) -> 'DXFObject': 61 """ Create new DXF object, add it to the entity database and to the entity space. 62 63 Args: 64 _type: DXF type like `DICTIONARY` 65 dxfattribs: DXF attributes as dict 66 67 (internal API) 68 """ 69 dxf_entity = factory.create_db_entry(_type, dxfattribs, self.doc) 70 self._entity_space.add(dxf_entity) 71 return dxf_entity 72 73 def delete_entity(self, entity: 'DXFObject') -> None: 74 """ Remove `entity` from entity space and destroy object. (internal API) """ 75 self._entity_space.remove(entity) 76 self.entitydb.delete_entity(entity) 77 78 def delete_all_entities(self) -> None: 79 """ Delete all DXF objects. (internal API) """ 80 db = self.entitydb 81 for entity in self._entity_space: 82 db.delete_entity(entity) 83 self._entity_space.clear() 84 85 def setup_rootdict(self) -> Dictionary: 86 """ Create a root dictionary. Has to be the first object in the objects section. (internal API) """ 87 if len(self): 88 raise DXFStructureError("Can not create root dictionary in none empty objects section.") 89 logger.debug('Creating ROOT dictionary.') 90 # root directory has no owner 91 return self.add_dictionary(owner='0') 92 93 def setup_objects_management_tables(self, rootdict: Dictionary) -> None: 94 """ Setup required management tables. (internal API)""" 95 96 def setup_plot_style_name_table(): 97 plot_style_name_dict = self.add_dictionary_with_default(owner=rootdict.dxf.handle) 98 placeholder = self.add_placeholder(owner=plot_style_name_dict.dxf.handle) 99 plot_style_name_dict.set_default(placeholder) 100 plot_style_name_dict['Normal'] = placeholder 101 rootdict['ACAD_PLOTSTYLENAME'] = plot_style_name_dict 102 103 for name in _OBJECT_TABLE_NAMES: 104 if name in rootdict: 105 continue # just create not existing tables 106 logger.info('creating {} dictionary'.format(name)) 107 if name == "ACAD_PLOTSTYLENAME": 108 setup_plot_style_name_table() 109 else: 110 rootdict.add_new_dict(name) 111 112 def add_object(self, entity: 'DXFObject') -> None: 113 """ Add `entity` to OBJECTS section. (internal API) """ 114 self._entity_space.add(entity) 115 116 def add_dxf_object_with_reactor(self, dxftype: str, dxfattribs: dict) -> 'DXFObject': 117 """ Add DXF object with reactor. (internal API) """ 118 dxfobject = self.new_entity(dxftype, dxfattribs) 119 dxfobject.set_reactors([dxfattribs['owner']]) 120 return dxfobject 121 122 def purge(self): 123 self._entity_space.purge() 124 125 # start of public interface 126 127 @property 128 def rootdict(self) -> Dictionary: 129 """ Root dictionary. """ 130 if len(self): 131 return self._entity_space[0] 132 else: 133 return self.setup_rootdict() 134 135 def __len__(self) -> int: 136 """ Returns count of DXF objects. """ 137 return len(self._entity_space) 138 139 def __iter__(self) -> Iterable['DXFObject']: 140 """ Returns iterable of all DXF objects in the OBJECTS section. """ 141 return iter(self._entity_space) 142 143 def __getitem__(self, index) -> 'DXFObject': 144 """ Get entity at `index`. 145 146 The underlying data structure for storing DXF objects is organized like a standard Python list, therefore `index` 147 can be any valid list indexing or slicing term, like a single index ``objects[-1]`` to get the last entity, or 148 an index slice ``objects[:10]`` to get the first 10 or less objects as ``List[DXFObject]``. 149 150 """ 151 return self._entity_space[index] # type: DXFObject 152 153 def __contains__(self, entity: Union['DXFObject', str]) -> bool: 154 """ Returns ``True`` if `entity` stored in OBJECTS section. 155 156 Args: 157 entity: :class:`DXFObject` or handle as hex string 158 159 """ 160 if isinstance(entity, str): 161 try: 162 entity = self.entitydb[entity] 163 except KeyError: 164 return False 165 return entity in self._entity_space 166 167 def query(self, query: str = '*') -> EntityQuery: 168 """ 169 Get all DXF objects matching the :ref:`entity query string`. 170 171 """ 172 return EntityQuery(iter(self), query) 173 174 def add_dictionary(self, owner: str = '0', hard_owned: bool = False) -> Dictionary: 175 """ Add new :class:`~ezdxf.entities.Dictionary` object. 176 177 Args: 178 owner: handle to owner as hex string. 179 hard_owned: ``True`` to treat entries as hard owned. 180 181 """ 182 entity = self.new_entity('DICTIONARY', dxfattribs={ 183 'owner': owner, 184 'hard_owned': hard_owned, 185 }) 186 return cast(Dictionary, entity) 187 188 def add_dictionary_with_default(self, owner='0', default='0', hard_owned: bool = False) -> 'DictionaryWithDefault': 189 """ Add new :class:`~ezdxf.entities.DictionaryWithDefault` object. 190 191 Args: 192 owner: handle to owner as hex string. 193 default: handle to default entry. 194 hard_owned: ``True`` to treat entries as hard owned. 195 196 """ 197 entity = self.new_entity('ACDBDICTIONARYWDFLT', dxfattribs={ 198 'owner': owner, 199 'default': default, 200 'hard_owned': hard_owned, 201 }) 202 return cast('DictionaryWithDefault', entity) 203 204 def add_dictionary_var(self, owner: str = '0', value: str = '') -> 'DictionaryVar': 205 """ 206 Add a new :class:`~ezdxf.entities.DictionaryVar` object. 207 208 Args: 209 owner: handle to owner as hex string. 210 value: value as string 211 212 """ 213 return self.new_entity('DICTIONARYVAR', dxfattribs={'owner': owner, 'value': value}) 214 215 def add_xrecord(self, owner: str = '0') -> 'XRecord': 216 """ 217 Add a new :class:`~ezdxf.entities.XRecord` object. 218 219 Args: 220 owner: handle to owner as hex string. 221 222 """ 223 return self.new_entity('XRECORD', dxfattribs={'owner': owner}) 224 225 def add_placeholder(self, owner: str = '0') -> 'Placeholder': 226 """ 227 Add a new :class:`~ezdxf.entities.Placeholder` object. 228 229 Args: 230 owner: handle to owner as hex string. 231 232 """ 233 return self.new_entity('ACDBPLACEHOLDER', dxfattribs={'owner': owner}) 234 235 # end of public interface 236 237 def set_raster_variables(self, frame: int = 0, quality: int = 1, units: str = 'm') -> None: 238 """ 239 Set raster variables. 240 241 Args: 242 frame: ``0`` = do not show image frame; ``1`` = show image frame 243 quality: ``0`` = draft; ``1`` = high 244 units: units for inserting images. This defines the real world unit for one drawing unit for the purpose of 245 inserting and scaling images with an associated resolution. 246 247 ===== =========================== 248 mm Millimeter 249 cm Centimeter 250 m Meter (ezdxf default) 251 km Kilometer 252 in Inch 253 ft Foot 254 yd Yard 255 mi Mile 256 ===== =========================== 257 258 (internal API), public interface :meth:`~ezdxf.drawing.Drawing.set_raster_variables` 259 260 """ 261 units = RASTER_UNITS.get(units, 0) 262 try: 263 raster_vars = self.rootdict['ACAD_IMAGE_VARS'] 264 except DXFKeyError: 265 raster_vars = self.add_dxf_object_with_reactor('RASTERVARIABLES', dxfattribs={ 266 'owner': self.rootdict.dxf.handle, 267 'frame': frame, 268 'quality': quality, 269 'units': units, 270 }) 271 self.rootdict['ACAD_IMAGE_VARS'] = raster_vars 272 else: 273 raster_vars.dxf.frame = frame 274 raster_vars.dxf.quality = quality 275 raster_vars.dxf.units = units 276 277 def set_wipeout_variables(self, frame: int = 0) -> None: 278 """ 279 Set wipeout variables. 280 281 Args: 282 frame: ``0`` = do not show image frame; ``1`` = show image frame 283 284 (internal API), public interface :meth:`~ezdxf.drawing.Drawing.set_wipeout_variables` 285 286 """ 287 try: 288 wipeout_vars = self.rootdict['ACAD_WIPEOUT_VARS'] 289 except DXFKeyError: 290 wipeout_vars = self.add_dxf_object_with_reactor('WIPEOUTVARIABLES', dxfattribs={ 291 'owner': self.rootdict.dxf.handle, 292 'frame': int(frame), 293 }) 294 self.rootdict['ACAD_WIPEOUT_VARS'] = wipeout_vars 295 else: 296 wipeout_vars.dxf.frame = int(frame) 297 298 def add_image_def(self, filename: str, size_in_pixel: Tuple[int, int], name=None) -> 'ImageDef': 299 """ 300 Add an image definition to the objects section. 301 302 Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing (objects section). `filename` is the image 303 file name as relative or absolute path and `size_in_pixel` is the image size in pixel as (x, y) tuple. To avoid 304 dependencies to external packages, `ezdxf` can not determine the image size by itself. Returns a 305 :class:`~ezdxf.entities.image.ImageDef` entity which is needed to create an image reference. `name` is the 306 internal image name, if set to ``None``, name is auto-generated. 307 308 Absolute image paths works best for AutoCAD but not really good, you have to update external references manually 309 in AutoCAD, which is not possible in TrueView. If the drawing units differ from 1 meter, you also have to use: 310 :meth:`set_raster_variables`. 311 312 Args: 313 filename: image file name (absolute path works best for AutoCAD) 314 size_in_pixel: image size in pixel as (x, y) tuple 315 name: image name for internal use, None for using filename as name (best for AutoCAD) 316 317 """ 318 # removed auto-generated name 319 # use absolute image paths for filename and AutoCAD loads images automatically 320 if name is None: 321 name = filename 322 image_dict = self.rootdict.get_required_dict('ACAD_IMAGE_DICT') 323 image_def = self.add_dxf_object_with_reactor('IMAGEDEF', dxfattribs={ 324 'owner': image_dict.dxf.handle, 325 'filename': filename, 326 'image_size': size_in_pixel, 327 }) 328 image_dict[name] = image_def.dxf.handle 329 return cast('ImageDef', image_def) 330 331 def add_image_def_reactor(self, image_handle: str) -> 'ImageDefReactor': 332 """ Add required IMAGEDEF_REACTOR object for IMAGEDEF object. (internal API) """ 333 image_def_reactor = self.new_entity('IMAGEDEF_REACTOR', dxfattribs={ 334 'owner': image_handle, 335 'image_handle': image_handle, 336 }) 337 return cast('ImageDefReactor', image_def_reactor) 338 339 def add_underlay_def(self, filename: str, format: str = 'pdf', name: str = None) -> 'UnderlayDef': 340 """ 341 Add an :class:`~ezdxf.entities.underlay.UnderlayDef` entity to the drawing (OBJECTS section). 342 `filename` is the underlay file name as relative or absolute path and `format` as string (pdf, dwf, dgn). 343 The underlay definition is required to create an underlay reference. 344 345 Args: 346 filename: underlay file name 347 format: file format as string ``'pdf'|'dwf'|'dgn'`` or ``'ext'`` for getting file format from filename extension 348 name: pdf format = page number to display; dgn format = ``'default'``; dwf: ???? 349 350 """ 351 fmt = format.upper() 352 if fmt in ('PDF', 'DWF', 'DGN'): 353 underlay_dict_name = 'ACAD_{}DEFINITIONS'.format(fmt) 354 underlay_def_entity = "{}DEFINITION".format(fmt) 355 else: 356 raise DXFValueError("Unsupported file format: '{}'".format(fmt)) 357 358 if name is None: 359 if fmt == 'PDF': 360 name = '1' # Display first page by default 361 elif fmt == 'DGN': 362 name = 'default' 363 else: 364 name = 'Model' # Display model space for DWF ??? 365 366 underlay_dict = self.rootdict.get_required_dict(underlay_dict_name) 367 underlay_def = self.new_entity(underlay_def_entity, dxfattribs={ 368 'owner': underlay_dict.dxf.handle, 369 'filename': filename, 370 'name': name, 371 }) 372 373 # auto-generated underlay key 374 key = self.next_underlay_key(lambda k: k not in underlay_dict) 375 underlay_dict[key] = underlay_def.dxf.handle 376 return cast('UnderlayDef', underlay_def) 377 378 def add_geodata(self, owner: str = '0', dxfattribs: dict = None) -> 'GeoData': 379 """ 380 Creates a new :class:`GeoData` entity and replaces existing ones. The GEODATA entity resides in the OBJECTS section 381 and NOT in the layout entity space and it is linked to the layout by an extension dictionary located in BLOCK_RECORD 382 of the layout. 383 384 The GEODATA entity requires DXF version R2010+. The DXF Reference does not document if other layouts than model 385 space supports geo referencing, so getting/setting geo data may only make sense for the model space layout, but 386 it is also available in paper space layouts. 387 388 Args: 389 owner: handle to owner as hex string 390 dxfattribs: DXF attributes for :class:`~ezdxf.entities.GeoData` entity 391 392 393 """ 394 if dxfattribs is None: 395 dxfattribs = {} 396 dxfattribs['owner'] = owner 397 return cast('GeoData', self.add_dxf_object_with_reactor('GEODATA', dxfattribs)) 398 399 400_OBJECT_TABLE_NAMES = [ 401 "ACAD_COLOR", 402 "ACAD_GROUP", 403 "ACAD_LAYOUT", 404 "ACAD_MATERIAL", 405 "ACAD_MLEADERSTYLE", 406 "ACAD_MLINESTYLE", 407 "ACAD_PLOTSETTINGS", 408 "ACAD_PLOTSTYLENAME", 409 "ACAD_SCALELIST", 410 "ACAD_TABLESTYLE", 411 "ACAD_VISUALSTYLE", 412] 413