1# Copyright (c) 2011-2020, Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Dict, Iterable, List, cast, Optional 4import logging 5from ezdxf.lldxf.const import DXFKeyError, DXFValueError, DXFInternalEzdxfError 6from ezdxf.lldxf.const import ( 7 MODEL_SPACE_R2000, PAPER_SPACE_R2000, 8 TMP_PAPER_SPACE_NAME, 9) 10from ezdxf.lldxf.validator import is_valid_table_name 11from .layout import Layout, Modelspace, Paperspace 12from ezdxf.entities import DXFEntity 13 14if TYPE_CHECKING: 15 from ezdxf.eztypes import Dictionary, Drawing, Auditor 16 17logger = logging.getLogger('ezdxf') 18 19 20def key(name: str) -> str: 21 """ AutoCAD uses case insensitive layout names, but stores the name case 22 sensitive. """ 23 return name.upper() 24 25 26MODEL = key('Model') 27 28 29class Layouts: 30 def __init__(self, doc: 'Drawing'): 31 """ Default constructor. (internal API) """ 32 self.doc = doc 33 # Store layout names in normalized form: key(name) 34 self._layouts: Dict[str, Layout] = {} 35 # key: layout name as original case sensitive string; value: DXFLayout() 36 self._dxf_layouts: 'Dictionary' = cast('Dictionary', 37 self.doc.rootdict['ACAD_LAYOUT']) 38 39 @classmethod 40 def setup(cls, doc: 'Drawing'): 41 """ Constructor from scratch. (internal API) """ 42 layouts = Layouts(doc) 43 layouts.setup_modelspace() 44 layouts.setup_paperspace() 45 return layouts 46 47 def __len__(self) -> int: 48 """ Returns count of existing layouts, including the modelspace 49 layout. """ 50 return len(self._layouts) 51 52 def __contains__(self, name: str) -> bool: 53 """ Returns ``True`` if layout `name` exist. """ 54 assert isinstance(name, str), type(str) 55 return key(name) in self._layouts 56 57 def __iter__(self) -> Iterable['Layout']: 58 """ Returns iterable of all layouts as :class:`~ezdxf.layouts.Layout` 59 objects, including the modelspace layout. 60 """ 61 return iter(self._layouts.values()) 62 63 def _add_layout(self, name: str, layout: Layout): 64 layout.dxf.name = name 65 self._layouts[key(name)] = layout 66 self._dxf_layouts[name] = layout.dxf_layout 67 68 def _discard(self, layout: 'Layout'): 69 name = layout.name 70 self._dxf_layouts.discard(name) 71 del self._layouts[key(name)] 72 73 def setup_modelspace(self): 74 """ Modelspace setup. (internal API) """ 75 self._new_special(Modelspace, 'Model', MODEL_SPACE_R2000, 76 dxfattribs={'taborder': 0}) 77 78 def setup_paperspace(self): 79 """ First layout setup. (internal API) """ 80 self._new_special(Paperspace, 'Layout1', PAPER_SPACE_R2000, 81 dxfattribs={'taborder': 1}) 82 83 def _new_special(self, cls, name: str, block_name: str, 84 dxfattribs: dict) -> 'Layout': 85 if name in self._layouts: 86 raise DXFValueError(f'Layout "{name}" already exists') 87 dxfattribs['owner'] = self._dxf_layouts.dxf.handle 88 layout = cls.new(name, block_name, self.doc, dxfattribs=dxfattribs) 89 self._add_layout(name, layout) 90 return layout 91 92 def unique_paperspace_name(self) -> str: 93 """ Returns a unique paperspace name. (internal API)""" 94 blocks = self.doc.blocks 95 count = 0 96 while "*Paper_Space%d" % count in blocks: 97 count += 1 98 return "*Paper_Space%d" % count 99 100 def new(self, name: str, dxfattribs: dict = None) -> Paperspace: 101 """ Returns a new :class:`~ezdxf.layouts.Paperspace` layout. 102 103 Args: 104 name: layout name as shown in tabs in :term:`CAD` applications 105 dxfattribs: additional DXF attributes for the 106 :class:`~ezdxf.entities.layout.DXFLayout` entity 107 108 Raises: 109 DXFValueError: Invalid characters in layout name. 110 DXFValueError: Layout `name` already exist. 111 112 """ 113 assert isinstance(name, str), type(str) 114 if not is_valid_table_name(name): 115 raise DXFValueError('Layout name contains invalid characters.') 116 117 if name in self: 118 raise DXFValueError(f'Layout "{name}" already exist.') 119 120 dxfattribs = dict(dxfattribs or {}) # copy attribs 121 dxfattribs['owner'] = self._dxf_layouts.dxf.handle 122 dxfattribs.setdefault('taborder', len(self._layouts) + 1) 123 block_name = self.unique_paperspace_name() 124 layout = Paperspace.new(name, block_name, self.doc, 125 dxfattribs=dxfattribs) 126 # Default extents are ok! 127 # Reset limits to (0, 0) and (paper width, paper height) 128 layout.reset_limits() 129 self._add_layout(name, layout) 130 return layout 131 132 @classmethod 133 def load(cls, doc: 'Drawing') -> 'Layouts': 134 """ Constructor if loading from file. (internal API) """ 135 layouts = cls(doc) 136 layouts.setup_from_rootdict() 137 138 # DXF R12: block/block_record for *Model_Space and *Paper_Space 139 # already exist: 140 if len(layouts) < 2: # restore missing DXF Layouts 141 layouts.restore('Model', MODEL_SPACE_R2000, taborder=0) 142 layouts.restore('Layout1', PAPER_SPACE_R2000, taborder=1) 143 return layouts 144 145 def restore(self, name: str, block_record_name: str, taborder: int) -> None: 146 """ Restore layout from block if DXFLayout does not exist. 147 (internal API) """ 148 if name in self: 149 return 150 block_layout = self.doc.blocks.get(block_record_name) 151 self._new_from_block_layout(name, block_layout, taborder) 152 153 def _new_from_block_layout(self, name, block_layout, 154 taborder: int) -> 'Layout': 155 dxfattribs = { 156 'owner': self._dxf_layouts.dxf.handle, 157 'name': name, 158 'block_record_handle': block_layout.block_record_handle, 159 'taborder': taborder, 160 } 161 dxf_layout = cast('DXFLayout', self.doc.objects.new_entity( 162 'LAYOUT', dxfattribs=dxfattribs)) 163 if key(name) == MODEL: 164 layout = Modelspace.load(dxf_layout, self.doc) 165 else: 166 layout = Paperspace.load(dxf_layout, self.doc) 167 self._add_layout(name, layout) 168 return layout 169 170 def setup_from_rootdict(self) -> None: 171 """ Setup layout manger from root dictionary. (internal API) """ 172 for name, dxf_layout in self._dxf_layouts.items(): 173 if key(name) == MODEL: 174 layout = Modelspace(dxf_layout, self.doc) 175 else: 176 layout = Paperspace(dxf_layout, self.doc) 177 # assert name == layout.dxf.name 178 self._layouts[key(name)] = layout 179 180 def modelspace(self) -> Modelspace: 181 """ Returns the :class:`~ezdxf.layouts.Modelspace` layout. """ 182 return cast(Modelspace, self.get('Model')) 183 184 def names(self) -> List[str]: 185 """ Returns a list of all layout names, all names in original case 186 sensitive form. """ 187 return [layout.name for layout in self._layouts.values()] 188 189 def get(self, name: Optional[str]) -> 'Layout': 190 """ Returns :class:`~ezdxf.layouts.Layout` by `name`, case insensitive 191 "Model" == "MODEL". 192 193 Args: 194 name: layout name as shown in tab, e.g. ``'Model'`` for modelspace 195 196 """ 197 name = name or self.names_in_taborder()[1] # first paperspace layout 198 return self._layouts[key(name)] 199 200 def rename(self, old_name: str, new_name: str) -> None: 201 """ Rename a layout from `old_name` to `new_name`. 202 Can not rename layout ``'Model'`` and the new name of a layout must 203 not exist. 204 205 Args: 206 old_name: actual layout name, case insensitive 207 new_name: new layout name, case insensitive 208 209 Raises: 210 DXFValueError: try to rename ``'Model'`` 211 DXFValueError: Layout `new_name` already exist. 212 213 """ 214 assert isinstance(old_name, str), type(old_name) 215 assert isinstance(new_name, str), type(new_name) 216 if key(old_name) == MODEL: 217 raise DXFValueError('Can not rename model space.') 218 if new_name in self: 219 raise DXFValueError(f'Layout "{new_name}" already exist.') 220 if old_name not in self: 221 raise DXFValueError(f'Layout "{old_name}" does not exist.') 222 223 layout = self.get(old_name) 224 self._discard(layout) 225 layout.rename(new_name) 226 self._add_layout(new_name, layout) 227 228 def names_in_taborder(self) -> List[str]: 229 """ Returns all layout names in tab order as shown in :term:`CAD` 230 applications. """ 231 names = [(layout.dxf.taborder, layout.name) for layout in 232 self._layouts.values()] 233 return [name for order, name in sorted(names)] 234 235 def get_layout_for_entity(self, entity: 'DXFEntity') -> 'Layout': 236 """ Returns the owner layout for a DXF `entity`. """ 237 owner = entity.dxf.owner 238 if owner is None: 239 raise DXFKeyError('No associated layout, owner is None.') 240 return self.get_layout_by_key(entity.dxf.owner) 241 242 def get_layout_by_key(self, layout_key: str) -> 'Layout': 243 """ Returns a layout by its `layout_key`. (internal API) """ 244 assert isinstance(layout_key, str), type(layout_key) 245 try: 246 block_record = self.doc.entitydb[layout_key] 247 dxf_layout = self.doc.entitydb[block_record.dxf.layout] 248 except KeyError: 249 raise DXFKeyError(f'Layout with key "{layout_key}" does not exist.') 250 return self.get(dxf_layout.dxf.name) 251 252 def get_active_layout_key(self): 253 """ Returns layout kay for the active paperspace layout. 254 (internal API) """ 255 active_layout_block_record = self.doc.block_records.get( 256 PAPER_SPACE_R2000) 257 return active_layout_block_record.dxf.handle 258 259 def set_active_layout(self, name: str) -> None: 260 """ Set layout `name` as active paperspace layout. """ 261 assert isinstance(name, str), type(name) 262 if key(name) == MODEL: # reserved layout name 263 raise DXFValueError('Can not set model space as active layout') 264 # raises KeyError if layout 'name' does not exist 265 new_active_layout = self.get(name) 266 old_active_layout_key = self.get_active_layout_key() 267 if old_active_layout_key == new_active_layout.layout_key: 268 return # layout 'name' is already the active layout 269 270 blocks = self.doc.blocks 271 new_active_paper_space_name = new_active_layout.block_record_name 272 273 blocks.rename_block(PAPER_SPACE_R2000, TMP_PAPER_SPACE_NAME) 274 blocks.rename_block(new_active_paper_space_name, PAPER_SPACE_R2000) 275 blocks.rename_block(TMP_PAPER_SPACE_NAME, new_active_paper_space_name) 276 277 def delete(self, name: str) -> None: 278 """ Delete layout `name` and destroy all entities in that layout. 279 280 Args: 281 name (str): layout name as shown in tabs 282 283 Raises: 284 DXFKeyError: if layout `name` do not exists 285 DXFValueError: deleting modelspace layout is not possible 286 DXFValueError: deleting last paperspace layout is not possible 287 288 """ 289 assert isinstance(name, str), type(name) 290 if key(name) == MODEL: 291 raise DXFValueError("Can not delete modelspace layout.") 292 293 layout = self.get(name) 294 if len(self) < 3: 295 raise DXFValueError("Can not delete last paperspace layout.") 296 if layout.layout_key == self.get_active_layout_key(): 297 # Layout `name` is the active layout: 298 for layout_name in self._layouts: 299 # Set any other paperspace layout as active layout 300 if layout_name not in (key(name), MODEL): 301 self.set_active_layout(layout_name) 302 break 303 self._discard(layout) 304 layout.destroy() 305 306 def active_layout(self) -> Paperspace: 307 """ Returns the active paperspace layout. """ 308 for layout in self: 309 if layout.is_active_paperspace: 310 return cast(Paperspace, layout) 311 raise DXFInternalEzdxfError('No active paperspace layout found.') 312 313 def audit(self, auditor: 'Auditor'): 314 from ezdxf.audit import AuditError 315 doc = auditor.doc 316 317 # Find/remove orphaned LAYOUT objects: 318 layouts = (o for o in doc.objects if o.dxftype() == 'LAYOUT') 319 for layout in layouts: 320 name = layout.dxf.get('name') 321 if name not in self: 322 auditor.fixed_error( 323 code=AuditError.ORPHANED_LAYOUT_ENTITY, 324 message=f'Removed orphaned {str(layout)} "{name}"' 325 ) 326 doc.objects.delete_entity(layout) 327 328 # Find/remove orphaned paperspace BLOCK_RECORDS named: *Paper_Space... 329 psp_br_handles = { 330 br.dxf.handle for br in doc.block_records if 331 br.dxf.name.lower().startswith('*paper_space') 332 } 333 psp_layout_br_handles = { 334 layout.dxf.block_record_handle for layout in 335 self._layouts.values() if key(layout.name) != MODEL 336 } 337 mismatch = psp_br_handles.difference(psp_layout_br_handles) 338 if len(mismatch): 339 for handle in mismatch: 340 br = doc.entitydb.get(handle) 341 name = br.dxf.get('name') 342 auditor.fixed_error( 343 code=AuditError.ORPHANED_PAPER_SPACE_BLOCK_RECORD_ENTITY, 344 message=f'Removed orphaned layout {str(br)} "{name}"' 345 ) 346 if name in doc.blocks: 347 doc.blocks.delete_block(name) 348 else: 349 doc.block_records.remove(name) 350