table.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # Purpose: tables contained in tables sections
  2. # Created: 13.03.2011
  3. # Copyright (c) 2011-2018, Manfred Moitzi
  4. # License: MIT License
  5. from typing import TYPE_CHECKING, Iterable, Iterator, Union, Optional, Sequence
  6. from ezdxf.lldxf.types import DXFTag
  7. from ezdxf.lldxf.tags import Tags
  8. from ezdxf.lldxf.extendedtags import ExtendedTags
  9. from ezdxf.lldxf.const import DXFTableEntryError, DXFStructureError, DXFAttributeError, Error
  10. if TYPE_CHECKING:
  11. from ezdxf.eztypes import Drawing, DXFEntity, DXFFactoryType, EntityDB, HandleGenerator, TagWriter
  12. TABLENAMES = {
  13. 'LAYER': 'LAYERS',
  14. 'LTYPE': 'LINETYPES',
  15. 'APPID': 'APPIDS',
  16. 'DIMSTYLE': 'DIMSTYLES',
  17. 'STYLE': 'STYLES',
  18. 'UCS': 'UCS',
  19. 'VIEW': 'VIEWS',
  20. 'VPORT': 'VIEWPORTS',
  21. 'BLOCK_RECORD': 'BLOCK_RECORDS',
  22. }
  23. def tablename(dxfname: str) -> str:
  24. """ Translate DXF-table-name to attribute-name. ('LAYER' -> 'LAYERS') """
  25. name = dxfname.upper()
  26. return TABLENAMES.get(name, name + 'S')
  27. class Table:
  28. def __init__(self, entities: Iterable[Tags], drawing: 'Drawing'):
  29. self._table_header = None
  30. self._dxfname = None
  31. self._drawing = drawing
  32. self._table_entries = []
  33. self._build_table_entries(iter(entities))
  34. # start public interface
  35. @staticmethod
  36. def key(entity: Union[str, 'DXFEntity']) -> str:
  37. if not isinstance(entity, str):
  38. entity = entity.dxf.name
  39. return entity.lower() # table key is lower case
  40. @property
  41. def name(self) -> str:
  42. return tablename(self._dxfname)
  43. def has_entry(self, name: str) -> bool:
  44. """ Check if an table-entry 'name' exists. """
  45. key = self.key(name)
  46. return any(self.key(entry) == key for entry in self)
  47. __contains__ = has_entry
  48. def new(self, name: str, dxfattribs: dict = None) -> 'DXFEntity':
  49. if self.has_entry(name):
  50. raise DXFTableEntryError('%s %s already exists!' % (self._dxfname, name))
  51. dxfattribs = dxfattribs or {}
  52. dxfattribs['name'] = name
  53. return self.new_entry(dxfattribs)
  54. def get(self, name: str) -> 'DXFEntity':
  55. """ Get table-entry by name as WrapperClass(). """
  56. key = self.key(name)
  57. for entry in iter(self):
  58. if self.key(entry) == key:
  59. return entry
  60. raise DXFTableEntryError(name)
  61. def remove(self, name: str) -> None:
  62. """ Remove table-entry from table and entitydb by name. """
  63. entry = self.get(name)
  64. handle = entry.dxf.handle
  65. self.remove_handle(handle)
  66. def __len__(self) -> int:
  67. return len(self._table_entries)
  68. def __iter__(self) -> Iterable['DXFEntity']:
  69. for handle in self._table_entries:
  70. yield self.get_table_entry_wrapper(handle)
  71. # end public interface
  72. def _build_table_entries(self, entities: Iterator[Tags]) -> None:
  73. table_head = next(entities)
  74. if table_head[0].value != 'TABLE':
  75. raise DXFStructureError("Critical structure error in TABLES section.")
  76. self._dxfname = table_head[1].value
  77. self._table_header = ExtendedTags(table_head) # do not store the table head in the entity database
  78. for table_entry in entities:
  79. self._append_entry_handle(table_entry.get_handle())
  80. @property
  81. def entitydb(self) -> 'EntityDB':
  82. return self._drawing.entitydb
  83. @property
  84. def handles(self) -> 'HandleGenerator':
  85. return self._drawing.entitydb.handles
  86. @property
  87. def dxffactory(self) -> 'DXFFactoryType':
  88. return self._drawing.dxffactory
  89. def _iter_table_entries_as_tags(self) -> Iterable[ExtendedTags]:
  90. """ Iterate over table-entries as Tags(). """
  91. return (self.entitydb[handle] for handle in self._table_entries)
  92. def new_entry(self, dxfattribs: dict) -> 'DXFEntity':
  93. """ Create new table-entry of type 'self._dxfname', and add new entry
  94. to table.
  95. Does not check if an entry dxfattribs['name'] already exists!
  96. Duplicate entries are possible for Viewports.
  97. """
  98. handle = self.handles.next()
  99. entry = self.dxffactory.new_entity(self._dxfname, handle, dxfattribs)
  100. self._add_entry(entry)
  101. return entry
  102. def duplicate_entry(self, name: str, new_name: str) -> 'DXFEntity':
  103. entry = self.get(name)
  104. xtags = self.entitydb.duplicate_tags(entry.tags)
  105. new_entity = self.dxffactory.wrap_entity(xtags)
  106. new_entity.dxf.name = new_name
  107. self._add_entry(new_entity)
  108. return new_entity
  109. def _add_entry(self, entry: Union[ExtendedTags, 'DXFEntity']) -> None:
  110. """ Add table-entry to table and entitydb. """
  111. if isinstance(entry, ExtendedTags):
  112. tags = entry
  113. else:
  114. tags = entry.tags
  115. handle = self.entitydb.add_tags(tags)
  116. self._append_entry_handle(handle)
  117. def _append_entry_handle(self, handle: str) -> None:
  118. if handle not in self._table_entries:
  119. self._table_entries.append(handle)
  120. def get_table_entry_wrapper(self, handle: str) -> 'DXFEntity':
  121. tags = self.entitydb[handle]
  122. return self.dxffactory.wrap_entity(tags)
  123. def write(self, tagwriter: 'TagWriter') -> None:
  124. """ Write DXF representation to stream, stream opened with mode='wt'. """
  125. def prologue():
  126. self._update_owner_handles()
  127. self._update_meta_data()
  128. tagwriter.write_tags(self._table_header)
  129. def content():
  130. for tags in self._iter_table_entries_as_tags():
  131. tagwriter.write_tags(tags)
  132. def epilogue():
  133. tagwriter.write_tag2(0, 'ENDTAB')
  134. prologue()
  135. content()
  136. epilogue()
  137. def _update_owner_handles(self) -> None:
  138. if self._drawing.dxfversion <= 'AC1009':
  139. return # no owner handles
  140. owner_handle = self._table_header.get_handle()
  141. for entry in iter(self):
  142. if not entry.supports_dxf_attrib('owner'):
  143. raise DXFAttributeError(repr(entry))
  144. entry.dxf.owner = owner_handle
  145. def _update_meta_data(self) -> None:
  146. count = len(self)
  147. if self._drawing.dxfversion > 'AC1009':
  148. subclass = self._table_header.get_subclass('AcDbSymbolTable')
  149. else:
  150. subclass = self._table_header.noclass
  151. subclass.set_first(DXFTag(70, count))
  152. def remove_handle(self, handle: str) -> None:
  153. """ Remove table-entry from table and entitydb by handle. """
  154. self._table_entries.remove(handle)
  155. del self.entitydb[handle]
  156. def audit(self, auditor) -> None:
  157. """
  158. Checks for table entries with same key.
  159. """
  160. entries = sorted(self._table_entries, key=lambda e: self.key(e))
  161. prev_key = None
  162. for entry in entries:
  163. key = self.key(entry)
  164. if key == prev_key:
  165. auditor.add_error(
  166. code=Error.DUPLICATE_TABLE_ENTRY_NAME,
  167. message="Duplicate table entry name '{1}' in table {0}".format(self.name, entry.dxf.name),
  168. dxf_entity=self,
  169. data=key,
  170. )
  171. prev_key = key
  172. class StyleTable(Table):
  173. def get_shx(self, shxname: str) -> 'DXFEntity':
  174. """
  175. Get existing shx entry, or create a new entry.
  176. Args:
  177. shxname: shape file name like 'ltypeshp.lin'
  178. """
  179. shape_file = self.find_shx(shxname)
  180. if shape_file is None:
  181. dxfattribs = {
  182. 'font': shxname,
  183. 'flags': 1,
  184. 'name': '', # shape file entry has no name
  185. 'last_height': 2.5, # just if this is required by AutoCAD
  186. }
  187. return self.new_entry(dxfattribs)
  188. else:
  189. return shape_file
  190. def find_shx(self, shxname: str) -> Optional['DXFEntity']:
  191. """
  192. Find .shx shape file table entry, by a case insensitive search.
  193. A .shx shape file table entry has no name, so you have to search by the font attribute.
  194. Args:
  195. shxname: .shx shape file name
  196. Returns:
  197. table entry or None if not found
  198. """
  199. lower_name = shxname.lower()
  200. for entry in iter(self):
  201. if entry.dxf.font.lower() == lower_name:
  202. return entry
  203. return None
  204. class ViewportTable(Table):
  205. # Viewport-Table can have multiple entries with same name
  206. def new(self, name: str, dxfattribs: dict = None):
  207. dxfattribs = dxfattribs or {}
  208. dxfattribs['name'] = name
  209. return self.new_entry(dxfattribs)
  210. def get_config(self, name: str) -> Sequence['DXFEntity']:
  211. key_func = self.key
  212. search_key = key_func(name)
  213. return [entry for entry in self if search_key == key_func(entry)]
  214. def delete_config(self, name: str) -> None:
  215. for entry in self.get_config(name):
  216. self.remove_handle(entry.dxf.handle)