drawing.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. # Created: 11.03.2011
  2. # Copyright (c) 2011-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, TextIO, Iterable
  5. from datetime import datetime
  6. import io
  7. import logging
  8. from itertools import chain
  9. from ezdxf.database import EntityDB
  10. from ezdxf.lldxf.const import DXFVersionError, acad_release, BLK_XREF, BLK_EXTERNAL, DXFValueError
  11. from ezdxf.lldxf.loader import load_dxf_structure, fill_database
  12. from ezdxf.dxffactory import dxffactory
  13. from ezdxf.templates import TemplateLoader
  14. from ezdxf.options import options
  15. from ezdxf.tools.codepage import tocodepage, toencoding
  16. from ezdxf.sections.sections import Sections
  17. from ezdxf.tools.juliandate import juliandate
  18. from ezdxf.lldxf import repair
  19. from ezdxf.tools import guid
  20. from ezdxf.tracker import Tracker
  21. from ezdxf.query import EntityQuery
  22. from ezdxf.groupby import groupby
  23. from ezdxf.render.dimension import DimensionRenderer
  24. logger = logging.getLogger('ezdxf')
  25. if TYPE_CHECKING:
  26. from .eztypes import HandleGenerator, DXFTag, LayoutType, SectionDict
  27. from .eztypes import GroupManager, MaterialManager, MLeaderStyleManager, MLineStyleManager
  28. from .eztypes import SectionType, HeaderSection, BlocksSection, Table, ViewportTable
  29. class Drawing:
  30. """
  31. The Central Data Object
  32. """
  33. def __init__(self, tagger: Iterable['DXFTag']):
  34. """
  35. Build a new DXF drawing from a steam of DXF tags.
  36. Args:
  37. tagger: generator or list of DXF tags as DXFTag() objects
  38. """
  39. def get_header(sections: 'SectionDict') -> 'SectionType':
  40. from .sections.header import HeaderSection
  41. header_entities = sections.get('HEADER', [None])[0] # all tags in the first DXF structure entity
  42. return HeaderSection(header_entities)
  43. self.tracker = Tracker()
  44. self._dimension_renderer = DimensionRenderer() # set DIMENSION rendering engine
  45. self._groups = None # type: GroupManager # read only
  46. self._materials = None # type: MaterialManager # read only
  47. self._mleader_styles = None # type: MLeaderStyleManager # read only
  48. self._mline_styles = None # type: MLineStyleManager # read only
  49. self._acad_compatible = True # will generated DXF file compatible with AutoCAD
  50. self._acad_incompatibility_reason = set() # avoid multiple warnings for same reason
  51. self.filename = None # type: str # read/write
  52. self.entitydb = EntityDB() # read only
  53. sections = load_dxf_structure(tagger) # load complete DXF entity structure
  54. # create section HEADER
  55. header = get_header(sections)
  56. self.dxfversion = header.get('$ACADVER', 'AC1009') # type: str # read only
  57. self.dxffactory = dxffactory(self) # read only, requires self.dxfversion
  58. self.encoding = toencoding(header.get('$DWGCODEPAGE', 'ANSI_1252')) # type: str # read/write
  59. # get handle seed
  60. seed = header.get('$HANDSEED', str(self.entitydb.handles)) # type: str
  61. # setup handles
  62. self.entitydb.handles.reset(seed)
  63. # store all necessary DXF entities in the drawing database
  64. fill_database(self.entitydb, sections, dxfversion=self.dxfversion)
  65. # create sections: TABLES, BLOCKS, ENTITIES, CLASSES, OBJECTS
  66. self.sections = Sections(sections, drawing=self, header=header)
  67. if self.dxfversion > 'AC1009':
  68. self.rootdict = self.objects.rootdict
  69. self.objects.setup_objects_management_tables(self.rootdict) # create missing tables
  70. if self.dxfversion in ('AC1012', 'AC1014'): # releases R13 and R14
  71. repair.upgrade_to_ac1015(self)
  72. # some applications don't setup properly the model and paper space layouts
  73. repair.setup_layouts(self)
  74. self._groups = self.objects.groups()
  75. self._materials = self.objects.materials()
  76. self._mleader_styles = self.objects.mleader_styles()
  77. self._mline_styles = self.objects.mline_styles()
  78. else: # dxfversion <= 'AC1009' do cleanup work, before building layouts
  79. if self.dxfversion < 'AC1009': # legacy DXF version
  80. repair.upgrade_to_ac1009(self) # upgrade to DXF format AC1009 (DXF R12)
  81. repair.cleanup_r12(self)
  82. # ezdxf puts automatically handles into all entities added to the entities database
  83. # write R12 without handles, by setting $HANDLING = 0
  84. self.header['$HANDLING'] = 1 # write handles by default
  85. self.layouts = self.dxffactory.get_layouts()
  86. @property
  87. def acad_release(self) -> str:
  88. return acad_release.get(self.dxfversion, "unknown")
  89. @property
  90. def acad_compatible(self) -> bool:
  91. return self._acad_compatible
  92. def add_acad_incompatibility_message(self, msg: str):
  93. self._acad_compatible = False
  94. if msg not in self._acad_incompatibility_reason:
  95. self._acad_incompatibility_reason.add(msg)
  96. logger.warning('Drawing is incompatible to AutoCAD, because {}.'.format(msg))
  97. @property
  98. def _handles(self) -> 'HandleGenerator':
  99. return self.entitydb.handles
  100. @property
  101. def header(self) -> 'HeaderSection':
  102. return self.sections.header
  103. @property
  104. def layers(self) -> 'Table':
  105. return self.sections.tables.layers
  106. @property
  107. def linetypes(self) -> 'Table':
  108. return self.sections.tables.linetypes
  109. @property
  110. def styles(self) -> 'Table':
  111. return self.sections.tables.styles
  112. @property
  113. def dimstyles(self) -> 'Table':
  114. return self.sections.tables.dimstyles
  115. @property
  116. def ucs(self) -> 'Table':
  117. return self.sections.tables.ucs
  118. @property
  119. def appids(self) -> 'Table':
  120. return self.sections.tables.appids
  121. @property
  122. def views(self) -> 'Table':
  123. return self.sections.tables.views
  124. @property
  125. def block_records(self) -> 'Table':
  126. return self.sections.tables.block_records
  127. @property
  128. def viewports(self) -> 'ViewportTable':
  129. return self.sections.tables.viewports
  130. @property
  131. def blocks(self) -> 'BlocksSection':
  132. return self.sections.blocks
  133. @property
  134. def groups(self) -> 'GroupManager':
  135. if self.dxfversion <= 'AC1009':
  136. raise DXFVersionError('Groups not supported in DXF version R12.')
  137. return self._groups
  138. @property
  139. def materials(self) -> 'MaterialManager':
  140. if self.dxfversion <= 'AC1009':
  141. raise DXFVersionError('Materials not supported in DXF version R12.')
  142. return self._materials
  143. @property
  144. def mleader_styles(self) -> 'MLeaderStyleManager':
  145. if self.dxfversion <= 'AC1009':
  146. raise DXFVersionError('MLeaderStyles not supported in DXF version R12.')
  147. return self._mleader_styles
  148. @property
  149. def mline_styles(self) -> 'MLineStyleManager':
  150. if self.dxfversion <= 'AC1009':
  151. raise DXFVersionError('MLineStyles not supported in DXF version R12.')
  152. return self._mline_styles
  153. @property
  154. def dimension_renderer(self) -> DimensionRenderer:
  155. return self._dimension_renderer
  156. @dimension_renderer.setter
  157. def dimension_renderer(self, renderer: DimensionRenderer) -> None:
  158. """
  159. Set your own dimension line renderer if needed.
  160. see also: ezdxf.render.dimension
  161. """
  162. self._dimension_renderer = renderer
  163. def modelspace(self) -> 'LayoutType':
  164. return self.layouts.modelspace()
  165. def layout(self, name: str = None) -> 'LayoutType':
  166. return self.layouts.get(name)
  167. def layout_names(self) -> Iterable[str]:
  168. return list(self.layouts.names())
  169. def delete_layout(self, name):
  170. if self.dxfversion > 'AC1009':
  171. if name not in self.layouts:
  172. raise DXFValueError("Layout '{}' does not exist.".format(name))
  173. else:
  174. self.layouts.delete(name)
  175. else:
  176. raise DXFVersionError('delete_layout() not supported for DXF version R12.')
  177. def new_layout(self, name, dxfattribs=None):
  178. if self.dxfversion > 'AC1009':
  179. if name in self.layouts:
  180. raise DXFValueError("Layout '{}' already exists.".format(name))
  181. else:
  182. return self.layouts.new(name, dxfattribs)
  183. else:
  184. raise DXFVersionError('new_layout() not supported for DXF version R12.')
  185. def layouts_and_blocks(self):
  186. """
  187. Iterate over all layouts (mode space and paper space) and all block definitions.
  188. Returns: yields Layout() objects
  189. """
  190. # DXF R12: model space and paper space layouts not linked into the associated BLOCK entity
  191. if self.dxfversion <= 'AC1009':
  192. return chain(self.layouts, self.blocks)
  193. # DXF R2000+: all layout spaces linked into their associated BLOCK entity
  194. else:
  195. return iter(self.blocks)
  196. def chain_layouts_and_blocks(self):
  197. """
  198. Chain entity spaces of all layouts and blocks. Yields an iterator for all entities in all layouts and blocks.
  199. Returns: yields all entities as DXFEntity() objects
  200. """
  201. layouts = list(self.layouts_and_blocks())
  202. return chain.from_iterable(layouts)
  203. def get_active_layout_key(self):
  204. if self.dxfversion > 'AC1009':
  205. try:
  206. active_layout_block_record = self.block_records.get('*Paper_Space') # block names are case insensitive
  207. return active_layout_block_record.dxf.handle
  208. except DXFValueError:
  209. return None
  210. else:
  211. return self.layout().layout_key # AC1009 supports just one layout and this is the active one
  212. def get_active_entity_space_layout_keys(self):
  213. layout_keys = [self.modelspace().layout_key]
  214. active_layout_key = self.get_active_layout_key()
  215. if active_layout_key is not None:
  216. layout_keys.append(active_layout_key)
  217. return layout_keys
  218. @property
  219. def entities(self):
  220. return self.sections.entities
  221. @property
  222. def objects(self):
  223. return self.sections.objects
  224. def get_dxf_entity(self, handle):
  225. """
  226. Get entity by *handle* from entity database.
  227. Low level access to DXF entities database. Raises *KeyError* if handle don't exists.
  228. Returns DXFEntity() or inherited.
  229. If you just need the raw DXF tags use::
  230. tags = Drawing.entitydb[handle] # raises KeyError, if handle don't exist
  231. tags = Drawing.entitydb.get(handle) # returns a default value, if handle don't exist (None by default)
  232. type of tags: ExtendedTags()
  233. """
  234. return self.dxffactory.wrap_handle(handle)
  235. def add_image_def(self, filename, size_in_pixel, name=None):
  236. """
  237. Add an image definition to the objects section.
  238. For AutoCAD works best with absolute image paths but not good, you have to update external references manually
  239. in AutoCAD, which is not possible in TrueView. If you drawing units differ from 1 meter, you also have to use:
  240. Drawing.set_raster_variables().
  241. Args:
  242. filename: image file name (absolute path works best for AutoCAD)
  243. size_in_pixel: image size in pixel as (x, y) tuple
  244. name: image name for internal use, None for using filename as name (best for AutoCAD)
  245. """
  246. if self.dxfversion < 'AC1015':
  247. raise DXFVersionError('The IMAGE entity needs at least DXF version R2000 or later.')
  248. if 'ACAD_IMAGE_VARS' not in self.rootdict:
  249. self.objects.set_raster_variables(frame=0, quality=1, units=3)
  250. if name is None:
  251. name = filename
  252. return self.objects.add_image_def(filename, size_in_pixel, name)
  253. def set_raster_variables(self, frame=0, quality=1, units='m'):
  254. """
  255. Set raster variables.
  256. Args:
  257. frame: 0 = do not show image frame; 1 = show image frame
  258. quality: 0 = draft; 1 = high
  259. units: units for inserting images. This is what one drawing unit is equal to for the purpose of inserting
  260. and scaling images with an associated resolution
  261. 'mm' = Millimeter
  262. 'cm' = Centimeter
  263. 'm' = Meter (ezdxf default)
  264. 'km' = Kilometer
  265. 'in' = Inch
  266. 'ft' = Foot
  267. 'yd' = Yard
  268. 'mi' = Mile
  269. everything else is None
  270. """
  271. self.objects.set_raster_variables(frame=frame, quality=quality, units=units)
  272. def set_wipeout_variables(self, frame=0):
  273. """
  274. Set wipeout variables.
  275. Args:
  276. frame: 0 = do not show image frame; 1 = show image frame
  277. """
  278. self.objects.set_wipeout_variables(frame=frame)
  279. def add_underlay_def(self, filename, format='ext', name=None):
  280. """
  281. Add an underlay definition to the objects section.
  282. Args:
  283. format: file format as string pdf|dwf|dgn or ext=get format from filename extension
  284. name: underlay name, None for an auto-generated name
  285. """
  286. if self.dxfversion < 'AC1015':
  287. raise DXFVersionError('The UNDERLAY entity needs at least DXF version R2000 or later.')
  288. if format == 'ext':
  289. format = filename[-3:]
  290. return self.objects.add_underlay_def(filename, format, name)
  291. def add_xref_def(self, filename, name, flags=BLK_XREF | BLK_EXTERNAL):
  292. """
  293. Add an external reference (xref) definition to the blocks section.
  294. Add xref to a layout by `layout.add_blockref(name, insert=(0, 0))`.
  295. Args:
  296. filename: external reference filename
  297. name: name of the xref block
  298. flags: block flags
  299. """
  300. self.blocks.new(name=name, dxfattribs={
  301. 'flags': flags,
  302. 'xref_path': filename
  303. })
  304. def _get_encoding(self):
  305. codepage = self.header.get('$DWGCODEPAGE', 'ANSI_1252')
  306. return toencoding(codepage)
  307. @staticmethod
  308. def new(dxfversion='AC1009'):
  309. from .lldxf.const import versions_supported_by_new, acad_release_to_dxf_version
  310. dxfversion = dxfversion.upper()
  311. dxfversion = acad_release_to_dxf_version.get(dxfversion, dxfversion) # translates 'R12' -> 'AC1009'
  312. if dxfversion not in versions_supported_by_new:
  313. raise DXFVersionError("Can not create DXF drawings, unsupported DXF version '{}'.".format(dxfversion))
  314. finder = TemplateLoader(options.template_dir)
  315. stream = finder.getstream(dxfversion.upper())
  316. try:
  317. dwg = Drawing.read(stream)
  318. finally:
  319. stream.close()
  320. dwg._setup_metadata()
  321. return dwg
  322. def _setup_metadata(self):
  323. self.header['$TDCREATE'] = juliandate(datetime.now())
  324. @staticmethod
  325. def read(stream: TextIO, legacy_mode: bool = False, dxfversion: str = None) -> 'Drawing':
  326. """ Open an existing drawing. """
  327. from .lldxf.tagger import low_level_tagger, tag_compiler
  328. tagger = low_level_tagger(stream)
  329. if legacy_mode:
  330. if dxfversion is not None and dxfversion <= 'AC1009':
  331. tagger = repair.filter_subclass_marker(tagger)
  332. tagger = repair.tag_reorder_layer(tagger)
  333. tagreader = tag_compiler(tagger)
  334. return Drawing(tagreader)
  335. def saveas(self, filename, encoding=None):
  336. self.filename = filename
  337. self.save(encoding=encoding)
  338. def save(self, encoding=None):
  339. # DXF R12, R2000, R2004 - ASCII encoding
  340. # DXF R2007 and newer - UTF-8 encoding
  341. if encoding is None:
  342. enc = 'utf-8' if self.dxfversion >= 'AC1021' else self.encoding
  343. else: # override default encoding, for applications that handles encoding different than AutoCAD
  344. enc = encoding
  345. # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode characters.
  346. with io.open(self.filename, mode='wt', encoding=enc, errors='dxfreplace') as fp:
  347. self.write(fp)
  348. def write(self, stream):
  349. from .lldxf.tagwriter import TagWriter
  350. if self.dxfversion == 'AC1009':
  351. handles = bool(self.header['$HANDLING'])
  352. else:
  353. handles = True
  354. if self.dxfversion > 'AC1009':
  355. self._register_required_classes()
  356. if self.dxfversion < 'AC1018':
  357. # remove unsupported group code 91
  358. repair.fix_classes(self)
  359. self._create_appids()
  360. self._update_metadata()
  361. tagwriter = TagWriter(stream, write_handles=handles)
  362. self.sections.write(tagwriter)
  363. def query(self, query='*'):
  364. """
  365. Entity query over all layouts and blocks.
  366. Excluding the OBJECTS section!
  367. Args:
  368. query: query string
  369. Returns: EntityQuery() container
  370. """
  371. return EntityQuery(self.chain_layouts_and_blocks(), query)
  372. def groupby(self, dxfattrib="", key=None):
  373. """
  374. Groups DXF entities of all layouts and blocks by an DXF attribute or a key function.
  375. Excluding the OBJECTS section!
  376. Args:
  377. dxfattrib: grouping DXF attribute like 'layer'
  378. key: key function, which accepts a DXFEntity as argument, returns grouping key of this entity or None for ignore
  379. this object. Reason for ignoring: a queried DXF attribute is not supported by this entity
  380. Returns: dict
  381. """
  382. return groupby(self.chain_layouts_and_blocks(), dxfattrib, key)
  383. def cleanup(self, groups=True):
  384. """
  385. Cleanup drawing. Call it before saving the drawing but only if necessary, the process could take a while.
  386. Args:
  387. groups (bool): removes deleted and invalid entities from groups
  388. """
  389. if groups and self.groups is not None:
  390. self.groups.cleanup()
  391. def auditor(self):
  392. """
  393. Get auditor for this drawing.
  394. Returns:
  395. Auditor() object
  396. """
  397. from ezdxf.audit.auditor import Auditor
  398. return Auditor(self)
  399. def validate(self, print_report=True):
  400. """
  401. Simple way to run an audit process.
  402. Args:
  403. print_report: print report to stdout
  404. Returns: True if no errors occurred else False
  405. """
  406. auditor = self.auditor()
  407. result = list(auditor.filter_zero_pointers(auditor.run()))
  408. if len(result):
  409. if print_report:
  410. auditor.print_report()
  411. return False
  412. else:
  413. return True
  414. def update_class_instance_counters(self):
  415. if 'classes' in self.sections:
  416. self._register_required_classes()
  417. self.sections.classes.update_instance_counters()
  418. def _register_required_classes(self):
  419. register = self.sections.classes.register
  420. for dxftype in self.tracker.dxftypes:
  421. cls = self.dxffactory.get_wrapper_class(dxftype)
  422. if cls.CLASS is not None:
  423. register(cls.CLASS)
  424. def _update_metadata(self):
  425. now = datetime.now()
  426. self.header['$TDUPDATE'] = juliandate(now)
  427. self.header['$HANDSEED'] = str(self.entitydb.handles)
  428. self.header['$DWGCODEPAGE'] = tocodepage(self.encoding)
  429. self.reset_versionguid()
  430. def _create_appids(self):
  431. def create_appid_if_not_exist(name, flags=0):
  432. if name not in self.appids:
  433. self.appids.new(name, {'flags': flags})
  434. if 'HATCH' in self.tracker.dxftypes:
  435. create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0)
  436. def reset_fingerprintguid(self):
  437. if self.dxfversion > 'AC1009':
  438. self.header['$FINGERPRINTGUID'] = guid()
  439. def reset_versionguid(self):
  440. if self.dxfversion > 'AC1009':
  441. self.header['$VERSIONGUID'] = guid()