123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- # Created: 11.03.2011
- # Copyright (c) 2011-2018, Manfred Moitzi
- # License: MIT License
- from typing import TYPE_CHECKING, TextIO, Iterable
- from datetime import datetime
- import io
- import logging
- from itertools import chain
- from ezdxf.database import EntityDB
- from ezdxf.lldxf.const import DXFVersionError, acad_release, BLK_XREF, BLK_EXTERNAL, DXFValueError
- from ezdxf.lldxf.loader import load_dxf_structure, fill_database
- from ezdxf.dxffactory import dxffactory
- from ezdxf.templates import TemplateLoader
- from ezdxf.options import options
- from ezdxf.tools.codepage import tocodepage, toencoding
- from ezdxf.sections.sections import Sections
- from ezdxf.tools.juliandate import juliandate
- from ezdxf.lldxf import repair
- from ezdxf.tools import guid
- from ezdxf.tracker import Tracker
- from ezdxf.query import EntityQuery
- from ezdxf.groupby import groupby
- from ezdxf.render.dimension import DimensionRenderer
- logger = logging.getLogger('ezdxf')
- if TYPE_CHECKING:
- from .eztypes import HandleGenerator, DXFTag, LayoutType, SectionDict
- from .eztypes import GroupManager, MaterialManager, MLeaderStyleManager, MLineStyleManager
- from .eztypes import SectionType, HeaderSection, BlocksSection, Table, ViewportTable
- class Drawing:
- """
- The Central Data Object
- """
- def __init__(self, tagger: Iterable['DXFTag']):
- """
- Build a new DXF drawing from a steam of DXF tags.
- Args:
- tagger: generator or list of DXF tags as DXFTag() objects
- """
- def get_header(sections: 'SectionDict') -> 'SectionType':
- from .sections.header import HeaderSection
- header_entities = sections.get('HEADER', [None])[0] # all tags in the first DXF structure entity
- return HeaderSection(header_entities)
- self.tracker = Tracker()
- self._dimension_renderer = DimensionRenderer() # set DIMENSION rendering engine
- self._groups = None # type: GroupManager # read only
- self._materials = None # type: MaterialManager # read only
- self._mleader_styles = None # type: MLeaderStyleManager # read only
- self._mline_styles = None # type: MLineStyleManager # read only
- self._acad_compatible = True # will generated DXF file compatible with AutoCAD
- self._acad_incompatibility_reason = set() # avoid multiple warnings for same reason
- self.filename = None # type: str # read/write
- self.entitydb = EntityDB() # read only
- sections = load_dxf_structure(tagger) # load complete DXF entity structure
- # create section HEADER
- header = get_header(sections)
- self.dxfversion = header.get('$ACADVER', 'AC1009') # type: str # read only
- self.dxffactory = dxffactory(self) # read only, requires self.dxfversion
- self.encoding = toencoding(header.get('$DWGCODEPAGE', 'ANSI_1252')) # type: str # read/write
- # get handle seed
- seed = header.get('$HANDSEED', str(self.entitydb.handles)) # type: str
- # setup handles
- self.entitydb.handles.reset(seed)
- # store all necessary DXF entities in the drawing database
- fill_database(self.entitydb, sections, dxfversion=self.dxfversion)
- # create sections: TABLES, BLOCKS, ENTITIES, CLASSES, OBJECTS
- self.sections = Sections(sections, drawing=self, header=header)
- if self.dxfversion > 'AC1009':
- self.rootdict = self.objects.rootdict
- self.objects.setup_objects_management_tables(self.rootdict) # create missing tables
- if self.dxfversion in ('AC1012', 'AC1014'): # releases R13 and R14
- repair.upgrade_to_ac1015(self)
- # some applications don't setup properly the model and paper space layouts
- repair.setup_layouts(self)
- self._groups = self.objects.groups()
- self._materials = self.objects.materials()
- self._mleader_styles = self.objects.mleader_styles()
- self._mline_styles = self.objects.mline_styles()
- else: # dxfversion <= 'AC1009' do cleanup work, before building layouts
- if self.dxfversion < 'AC1009': # legacy DXF version
- repair.upgrade_to_ac1009(self) # upgrade to DXF format AC1009 (DXF R12)
- repair.cleanup_r12(self)
- # ezdxf puts automatically handles into all entities added to the entities database
- # write R12 without handles, by setting $HANDLING = 0
- self.header['$HANDLING'] = 1 # write handles by default
- self.layouts = self.dxffactory.get_layouts()
- @property
- def acad_release(self) -> str:
- return acad_release.get(self.dxfversion, "unknown")
- @property
- def acad_compatible(self) -> bool:
- return self._acad_compatible
- def add_acad_incompatibility_message(self, msg: str):
- self._acad_compatible = False
- if msg not in self._acad_incompatibility_reason:
- self._acad_incompatibility_reason.add(msg)
- logger.warning('Drawing is incompatible to AutoCAD, because {}.'.format(msg))
- @property
- def _handles(self) -> 'HandleGenerator':
- return self.entitydb.handles
- @property
- def header(self) -> 'HeaderSection':
- return self.sections.header
- @property
- def layers(self) -> 'Table':
- return self.sections.tables.layers
- @property
- def linetypes(self) -> 'Table':
- return self.sections.tables.linetypes
- @property
- def styles(self) -> 'Table':
- return self.sections.tables.styles
- @property
- def dimstyles(self) -> 'Table':
- return self.sections.tables.dimstyles
- @property
- def ucs(self) -> 'Table':
- return self.sections.tables.ucs
- @property
- def appids(self) -> 'Table':
- return self.sections.tables.appids
- @property
- def views(self) -> 'Table':
- return self.sections.tables.views
- @property
- def block_records(self) -> 'Table':
- return self.sections.tables.block_records
- @property
- def viewports(self) -> 'ViewportTable':
- return self.sections.tables.viewports
- @property
- def blocks(self) -> 'BlocksSection':
- return self.sections.blocks
- @property
- def groups(self) -> 'GroupManager':
- if self.dxfversion <= 'AC1009':
- raise DXFVersionError('Groups not supported in DXF version R12.')
- return self._groups
- @property
- def materials(self) -> 'MaterialManager':
- if self.dxfversion <= 'AC1009':
- raise DXFVersionError('Materials not supported in DXF version R12.')
- return self._materials
- @property
- def mleader_styles(self) -> 'MLeaderStyleManager':
- if self.dxfversion <= 'AC1009':
- raise DXFVersionError('MLeaderStyles not supported in DXF version R12.')
- return self._mleader_styles
- @property
- def mline_styles(self) -> 'MLineStyleManager':
- if self.dxfversion <= 'AC1009':
- raise DXFVersionError('MLineStyles not supported in DXF version R12.')
- return self._mline_styles
- @property
- def dimension_renderer(self) -> DimensionRenderer:
- return self._dimension_renderer
- @dimension_renderer.setter
- def dimension_renderer(self, renderer: DimensionRenderer) -> None:
- """
- Set your own dimension line renderer if needed.
- see also: ezdxf.render.dimension
- """
- self._dimension_renderer = renderer
- def modelspace(self) -> 'LayoutType':
- return self.layouts.modelspace()
- def layout(self, name: str = None) -> 'LayoutType':
- return self.layouts.get(name)
- def layout_names(self) -> Iterable[str]:
- return list(self.layouts.names())
- def delete_layout(self, name):
- if self.dxfversion > 'AC1009':
- if name not in self.layouts:
- raise DXFValueError("Layout '{}' does not exist.".format(name))
- else:
- self.layouts.delete(name)
- else:
- raise DXFVersionError('delete_layout() not supported for DXF version R12.')
- def new_layout(self, name, dxfattribs=None):
- if self.dxfversion > 'AC1009':
- if name in self.layouts:
- raise DXFValueError("Layout '{}' already exists.".format(name))
- else:
- return self.layouts.new(name, dxfattribs)
- else:
- raise DXFVersionError('new_layout() not supported for DXF version R12.')
- def layouts_and_blocks(self):
- """
- Iterate over all layouts (mode space and paper space) and all block definitions.
- Returns: yields Layout() objects
- """
- # DXF R12: model space and paper space layouts not linked into the associated BLOCK entity
- if self.dxfversion <= 'AC1009':
- return chain(self.layouts, self.blocks)
- # DXF R2000+: all layout spaces linked into their associated BLOCK entity
- else:
- return iter(self.blocks)
- def chain_layouts_and_blocks(self):
- """
- Chain entity spaces of all layouts and blocks. Yields an iterator for all entities in all layouts and blocks.
- Returns: yields all entities as DXFEntity() objects
- """
- layouts = list(self.layouts_and_blocks())
- return chain.from_iterable(layouts)
- def get_active_layout_key(self):
- if self.dxfversion > 'AC1009':
- try:
- active_layout_block_record = self.block_records.get('*Paper_Space') # block names are case insensitive
- return active_layout_block_record.dxf.handle
- except DXFValueError:
- return None
- else:
- return self.layout().layout_key # AC1009 supports just one layout and this is the active one
- def get_active_entity_space_layout_keys(self):
- layout_keys = [self.modelspace().layout_key]
- active_layout_key = self.get_active_layout_key()
- if active_layout_key is not None:
- layout_keys.append(active_layout_key)
- return layout_keys
- @property
- def entities(self):
- return self.sections.entities
- @property
- def objects(self):
- return self.sections.objects
- def get_dxf_entity(self, handle):
- """
- Get entity by *handle* from entity database.
- Low level access to DXF entities database. Raises *KeyError* if handle don't exists.
- Returns DXFEntity() or inherited.
- If you just need the raw DXF tags use::
- tags = Drawing.entitydb[handle] # raises KeyError, if handle don't exist
- tags = Drawing.entitydb.get(handle) # returns a default value, if handle don't exist (None by default)
- type of tags: ExtendedTags()
- """
- return self.dxffactory.wrap_handle(handle)
- def add_image_def(self, filename, size_in_pixel, name=None):
- """
- Add an image definition to the objects section.
- For AutoCAD works best with absolute image paths but not good, you have to update external references manually
- in AutoCAD, which is not possible in TrueView. If you drawing units differ from 1 meter, you also have to use:
- Drawing.set_raster_variables().
- Args:
- filename: image file name (absolute path works best for AutoCAD)
- size_in_pixel: image size in pixel as (x, y) tuple
- name: image name for internal use, None for using filename as name (best for AutoCAD)
- """
- if self.dxfversion < 'AC1015':
- raise DXFVersionError('The IMAGE entity needs at least DXF version R2000 or later.')
- if 'ACAD_IMAGE_VARS' not in self.rootdict:
- self.objects.set_raster_variables(frame=0, quality=1, units=3)
- if name is None:
- name = filename
- return self.objects.add_image_def(filename, size_in_pixel, name)
- def set_raster_variables(self, frame=0, quality=1, units='m'):
- """
- Set raster variables.
- Args:
- frame: 0 = do not show image frame; 1 = show image frame
- quality: 0 = draft; 1 = high
- units: units for inserting images. This is what one drawing unit is equal to for the purpose of inserting
- and scaling images with an associated resolution
- 'mm' = Millimeter
- 'cm' = Centimeter
- 'm' = Meter (ezdxf default)
- 'km' = Kilometer
- 'in' = Inch
- 'ft' = Foot
- 'yd' = Yard
- 'mi' = Mile
- everything else is None
- """
- self.objects.set_raster_variables(frame=frame, quality=quality, units=units)
- def set_wipeout_variables(self, frame=0):
- """
- Set wipeout variables.
- Args:
- frame: 0 = do not show image frame; 1 = show image frame
- """
- self.objects.set_wipeout_variables(frame=frame)
- def add_underlay_def(self, filename, format='ext', name=None):
- """
- Add an underlay definition to the objects section.
- Args:
- format: file format as string pdf|dwf|dgn or ext=get format from filename extension
- name: underlay name, None for an auto-generated name
- """
- if self.dxfversion < 'AC1015':
- raise DXFVersionError('The UNDERLAY entity needs at least DXF version R2000 or later.')
- if format == 'ext':
- format = filename[-3:]
- return self.objects.add_underlay_def(filename, format, name)
- def add_xref_def(self, filename, name, flags=BLK_XREF | BLK_EXTERNAL):
- """
- Add an external reference (xref) definition to the blocks section.
- Add xref to a layout by `layout.add_blockref(name, insert=(0, 0))`.
- Args:
- filename: external reference filename
- name: name of the xref block
- flags: block flags
- """
- self.blocks.new(name=name, dxfattribs={
- 'flags': flags,
- 'xref_path': filename
- })
- def _get_encoding(self):
- codepage = self.header.get('$DWGCODEPAGE', 'ANSI_1252')
- return toencoding(codepage)
- @staticmethod
- def new(dxfversion='AC1009'):
- from .lldxf.const import versions_supported_by_new, acad_release_to_dxf_version
- dxfversion = dxfversion.upper()
- dxfversion = acad_release_to_dxf_version.get(dxfversion, dxfversion) # translates 'R12' -> 'AC1009'
- if dxfversion not in versions_supported_by_new:
- raise DXFVersionError("Can not create DXF drawings, unsupported DXF version '{}'.".format(dxfversion))
- finder = TemplateLoader(options.template_dir)
- stream = finder.getstream(dxfversion.upper())
- try:
- dwg = Drawing.read(stream)
- finally:
- stream.close()
- dwg._setup_metadata()
- return dwg
- def _setup_metadata(self):
- self.header['$TDCREATE'] = juliandate(datetime.now())
- @staticmethod
- def read(stream: TextIO, legacy_mode: bool = False, dxfversion: str = None) -> 'Drawing':
- """ Open an existing drawing. """
- from .lldxf.tagger import low_level_tagger, tag_compiler
- tagger = low_level_tagger(stream)
- if legacy_mode:
- if dxfversion is not None and dxfversion <= 'AC1009':
- tagger = repair.filter_subclass_marker(tagger)
- tagger = repair.tag_reorder_layer(tagger)
- tagreader = tag_compiler(tagger)
- return Drawing(tagreader)
- def saveas(self, filename, encoding=None):
- self.filename = filename
- self.save(encoding=encoding)
- def save(self, encoding=None):
- # DXF R12, R2000, R2004 - ASCII encoding
- # DXF R2007 and newer - UTF-8 encoding
- if encoding is None:
- enc = 'utf-8' if self.dxfversion >= 'AC1021' else self.encoding
- else: # override default encoding, for applications that handles encoding different than AutoCAD
- enc = encoding
- # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode characters.
- with io.open(self.filename, mode='wt', encoding=enc, errors='dxfreplace') as fp:
- self.write(fp)
- def write(self, stream):
- from .lldxf.tagwriter import TagWriter
- if self.dxfversion == 'AC1009':
- handles = bool(self.header['$HANDLING'])
- else:
- handles = True
- if self.dxfversion > 'AC1009':
- self._register_required_classes()
- if self.dxfversion < 'AC1018':
- # remove unsupported group code 91
- repair.fix_classes(self)
- self._create_appids()
- self._update_metadata()
- tagwriter = TagWriter(stream, write_handles=handles)
- self.sections.write(tagwriter)
- def query(self, query='*'):
- """
- Entity query over all layouts and blocks.
- Excluding the OBJECTS section!
- Args:
- query: query string
- Returns: EntityQuery() container
- """
- return EntityQuery(self.chain_layouts_and_blocks(), query)
- def groupby(self, dxfattrib="", key=None):
- """
- Groups DXF entities of all layouts and blocks by an DXF attribute or a key function.
- Excluding the OBJECTS section!
- Args:
- dxfattrib: grouping DXF attribute like 'layer'
- key: key function, which accepts a DXFEntity as argument, returns grouping key of this entity or None for ignore
- this object. Reason for ignoring: a queried DXF attribute is not supported by this entity
- Returns: dict
- """
- return groupby(self.chain_layouts_and_blocks(), dxfattrib, key)
- def cleanup(self, groups=True):
- """
- Cleanup drawing. Call it before saving the drawing but only if necessary, the process could take a while.
- Args:
- groups (bool): removes deleted and invalid entities from groups
- """
- if groups and self.groups is not None:
- self.groups.cleanup()
- def auditor(self):
- """
- Get auditor for this drawing.
- Returns:
- Auditor() object
- """
- from ezdxf.audit.auditor import Auditor
- return Auditor(self)
- def validate(self, print_report=True):
- """
- Simple way to run an audit process.
- Args:
- print_report: print report to stdout
- Returns: True if no errors occurred else False
- """
- auditor = self.auditor()
- result = list(auditor.filter_zero_pointers(auditor.run()))
- if len(result):
- if print_report:
- auditor.print_report()
- return False
- else:
- return True
- def update_class_instance_counters(self):
- if 'classes' in self.sections:
- self._register_required_classes()
- self.sections.classes.update_instance_counters()
- def _register_required_classes(self):
- register = self.sections.classes.register
- for dxftype in self.tracker.dxftypes:
- cls = self.dxffactory.get_wrapper_class(dxftype)
- if cls.CLASS is not None:
- register(cls.CLASS)
- def _update_metadata(self):
- now = datetime.now()
- self.header['$TDUPDATE'] = juliandate(now)
- self.header['$HANDSEED'] = str(self.entitydb.handles)
- self.header['$DWGCODEPAGE'] = tocodepage(self.encoding)
- self.reset_versionguid()
- def _create_appids(self):
- def create_appid_if_not_exist(name, flags=0):
- if name not in self.appids:
- self.appids.new(name, {'flags': flags})
- if 'HATCH' in self.tracker.dxftypes:
- create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0)
- def reset_fingerprintguid(self):
- if self.dxfversion > 'AC1009':
- self.header['$FINGERPRINTGUID'] = guid()
- def reset_versionguid(self):
- if self.dxfversion > 'AC1009':
- self.header['$VERSIONGUID'] = guid()
|