コード例 #1
0
def test_empty_section():
    sec = load_section(EMPTYSEC, "CLASSES")
    cls_entities = [factory.load(ExtendedTags(e)) for e in sec]

    section = ClassesSection(None, iter(cls_entities))
    stream = StringIO()
    section.export_dxf(TagWriter(stream))
    result = stream.getvalue()
    stream.close()
    assert EMPTYSEC == result
コード例 #2
0
def test_export_dxf():
    classes = ClassesSection()
    classes.add_class("SUN")
    collector = TagCollector(dxfversion=ezdxf.DXF2004)
    classes.export_dxf(collector)
    tags = collector.tags
    assert tags[0] == (0, "SECTION")
    assert tags[1] == (2, "CLASSES")
    assert tags[2] == (0, "CLASS")
    # writing classes is tested in 'test_113_dxfclass.py'
    assert tags[-1] == (0, "ENDSEC")
コード例 #3
0
def test_empty_section():
    doc = Drawing()
    sec = load_section(EMPTYSEC, 'CLASSES')
    cls_entities = [doc.dxffactory.entity_from_tags(e) for e in sec]

    section = ClassesSection(None, iter(cls_entities))
    stream = StringIO()
    section.export_dxf(TagWriter(stream))
    result = stream.getvalue()
    stream.close()
    assert EMPTYSEC == result
コード例 #4
0
ファイル: test_405_classes.py プロジェクト: vshu3000/ezdxf
def test_export_dxf():
    classes = ClassesSection()
    classes.add_class('SUN')
    collector = TagCollector(dxfversion=ezdxf.DXF2004)
    classes.export_dxf(collector)
    tags = collector.tags
    assert tags[0] == (0, 'SECTION')
    assert tags[1] == (2, 'CLASSES')
    assert tags[2] == (0, 'CLASS')
    # writing classes is tested in 'test_113_dxfclass.py'
    assert tags[-1] == (0, 'ENDSEC')
コード例 #5
0
class Drawing:
    def __init__(self, dxfversion=DXF2013):
        self.entitydb = EntityDB()
        target_dxfversion = dxfversion.upper()
        self._dxfversion: str = const.acad_release_to_dxf_version.get(
            target_dxfversion, target_dxfversion)
        if self._dxfversion not in const.versions_supported_by_new:
            raise const.DXFVersionError(
                f'Unsupported DXF version "{self.dxfversion}".')
        # Store original dxf version if loaded (and maybe converted R13/14)
        # from file.
        self._loaded_dxfversion: Optional[str] = None

        # Status flag which is True while loading content from a DXF file:
        self.is_loading = False
        self.encoding: str = 'cp1252'  # read/write
        self.filename: Optional[str] = None

        # named objects dictionary
        self.rootdict: 'Dictionary' = None

        # DXF sections
        self.header: HeaderSection = None
        self.classes: ClassesSection = None
        self.tables: TablesSection = None
        self.blocks: BlocksSection = None
        self.entities: EntitySection = None
        self.objects: ObjectsSection = None

        # DXF R2013 and later
        self.acdsdata: AcDsDataSection = None

        self.stored_sections = []
        self.layouts: Layouts = None
        self.groups: GroupCollection = None
        self.materials: MaterialCollection = None
        self.mleader_styles: MLeaderStyleCollection = None
        self.mline_styles: MLineStyleCollection = None

        # Set to False if the generated DXF file will be incompatible to AutoCAD
        self._acad_compatible = True
        # Store reasons for AutoCAD incompatibility:
        self._acad_incompatibility_reason = set()

        # DIMENSION rendering engine can be replaced by a custom Dimension
        # render: see property Drawing.dimension_renderer
        self._dimension_renderer = DimensionRenderer()

        # Some fixes can't be applied while the DXF document is not fully
        # initialized, store this fixes as callable object:
        self._post_init_commands: List[Callable] = []
        # Don't create any new entities here:
        # New created handles could collide with handles loaded from DXF file.
        assert len(self.entitydb) == 0

    @classmethod
    def new(cls, dxfversion: str = DXF2013) -> 'Drawing':
        """ Create new drawing. Package users should use the factory function
        :func:`ezdxf.new`. (internal API)
        """
        doc = cls(dxfversion)
        doc._setup()
        return doc

    def _setup(self):
        self.header = HeaderSection.new()
        self.classes = ClassesSection(self)
        self.tables = TablesSection(self)
        self.blocks = BlocksSection(self)
        self.entities = EntitySection(self)
        self.objects = ObjectsSection(self)
        # AcDSData section is not supported for new drawings
        self.acdsdata = AcDsDataSection(self)
        self.rootdict = self.objects.rootdict
        # Create missing tables:
        self.objects.setup_objects_management_tables(self.rootdict)
        self.layouts = Layouts.setup(self)
        self._finalize_setup()

    def _finalize_setup(self):
        """ Common setup tasks for new and loaded DXF drawings. """
        self.groups = GroupCollection(self)
        self.materials = MaterialCollection(self)

        self.mline_styles = MLineStyleCollection(self)
        # all required internal structures are ready
        # now do the stuff to please AutoCAD
        self._create_required_table_entries()

        # mleader_styles requires text styles
        self.mleader_styles = MLeaderStyleCollection(self)
        self._set_required_layer_attributes()
        self._setup_metadata()
        self._execute_post_init_commands()

    def _execute_post_init_commands(self):
        for cmd in self._post_init_commands:
            cmd()
        del self._post_init_commands

    def _create_required_table_entries(self):
        self._create_required_vports()
        self._create_required_linetypes()
        self._create_required_layers()
        self._create_required_styles()
        self._create_required_appids()
        self._create_required_dimstyles()

    def _set_required_layer_attributes(self):
        for layer in self.layers:  # type: Layer
            layer.set_required_attributes()

    def _create_required_vports(self):
        if '*Active' not in self.viewports:
            self.viewports.new('*Active')

    def _create_required_appids(self):
        if 'ACAD' not in self.appids:
            self.appids.new('ACAD')

    def _create_required_linetypes(self):
        linetypes = self.linetypes
        for name in ('ByBlock', 'ByLayer', 'Continuous'):
            if name not in linetypes:
                linetypes.new(name)

    def _create_required_dimstyles(self):
        if 'Standard' not in self.dimstyles:
            self.dimstyles.new('Standard')

    def _create_required_styles(self):
        if 'Standard' not in self.styles:
            self.styles.new('Standard')

    def _create_required_layers(self):
        layers = self.layers
        if '0' not in layers:
            layers.new('0')
        if 'Defpoints' not in layers:
            layers.new('Defpoints', dxfattribs={'plot': 0})  # do not plot
        else:
            # AutoCAD requires a plot flag = 0
            layers.get('Defpoints').dxf.plot = 0

    def _setup_metadata(self):
        self.header['$ACADVER'] = self.dxfversion
        self.header['$TDCREATE'] = juliandate(datetime.now())
        self.reset_fingerprint_guid()
        self.reset_version_guid()

    @property
    def dxfversion(self) -> str:
        """ Get current DXF version. """
        return self._dxfversion

    @dxfversion.setter
    def dxfversion(self, version) -> None:
        """ Set current DXF version. """
        self._dxfversion = self._validate_dxf_version(version)
        self.header['$ACADVER'] = version

    @property
    def output_encoding(self):
        """ Returns required output encoding for writing document to a text
        streams.
        """
        return 'utf-8' if self.dxfversion >= DXF2007 else self.encoding

    @property
    def units(self) -> int:
        """ Get and set the document/modelspace base units as enum, for more
        information read this: :ref:`dxf units`.

        """
        return self.header.get('$INSUNITS', 0)

    @units.setter
    def units(self, unit_enum: int) -> None:
        if 0 <= unit_enum < 25:
            self.header['$INSUNITS'] = unit_enum
        else:
            raise ValueError(f'Invalid units enum: {unit_enum}')

    def _validate_dxf_version(self, version: str) -> str:
        version = version.upper()
        # translates 'R12' -> 'AC1009'
        version = const.acad_release_to_dxf_version.get(version, version)
        if version not in const.versions_supported_by_save:
            raise const.DXFVersionError(
                f'Unsupported DXF version "{version}".')
        if version == DXF12:
            if self._dxfversion > DXF12:
                logger.warning(
                    f'Downgrade from DXF {self.acad_release} to R12 may create '
                    f'an invalid DXF file.')
        elif version < self._dxfversion:
            logger.info(
                f'Downgrade from DXF {self.acad_release} to '
                f'{const.acad_release[version]} can cause lost of features.')
        return version

    @classmethod
    def read(cls, stream: TextIO) -> 'Drawing':
        """ Open an existing drawing. Package users should use the factory
        function :func:`ezdxf.read`. To preserve possible binary data in
        XRECORD entities use :code:`errors='surrogateescape'` as error handler
        for the import stream.

        Args:
             stream: text stream yielding text (unicode) strings by readline()

        """
        from .lldxf.tagger import ascii_tags_loader
        tag_loader = ascii_tags_loader(stream)
        return cls.load(tag_loader)

    @classmethod
    def load(cls, tag_loader: Iterable['DXFTag']) -> 'Drawing':
        """ Load DXF document from a DXF tag loader, in general an external
        untrusted source.

        Args:
            tag_loader: DXF tag loader

        """
        from .lldxf.tagger import tag_compiler
        tag_loader = tag_compiler(tag_loader)
        doc = cls()
        doc._load(tag_loader)
        return doc

    @classmethod
    def from_tags(cls, compiled_tags: Iterable['DXFTag']) -> 'Drawing':
        """ Create new drawing from compiled tags. (internal API)"""
        doc = cls()
        doc._load(tagger=compiled_tags)
        return doc

    def _load(self, tagger: Optional[Iterable['DXFTag']]) -> None:
        # 1st Loading stage: load complete DXF entity structure
        self.is_loading = True
        sections = loader.load_dxf_structure(tagger)
        if 'THUMBNAILIMAGE' in sections:
            del sections['THUMBNAILIMAGE']
        self._load_section_dict(sections)

    def _load_section_dict(self, sections: loader.SectionDict) -> None:
        """ Internal API to load a DXF document from a section dict. """
        self.is_loading = True
        # Create header section:
        # All header tags are the first DXF structure entity
        header_entities = sections.get('HEADER', [None])[0]
        if header_entities is None:
            # Create default header, files without header are by default DXF R12
            self.header = HeaderSection.new(dxfversion=DXF12)
        else:
            self.header = HeaderSection.load(header_entities)

        self._dxfversion: str = self.header.get('$ACADVER', DXF12)

        # Store original DXF version of loaded file.
        self._loaded_dxfversion = self._dxfversion

        # Content encoding:
        self.encoding = toencoding(self.header.get('$DWGCODEPAGE',
                                                   'ANSI_1252'))

        # Set handle seed:
        seed: str = self.header.get('$HANDSEED', str(self.entitydb.handles))
        self.entitydb.handles.reset(_validate_handle_seed(seed))

        # Store all necessary DXF entities in the entity database:
        loader.load_and_bind_dxf_content(sections, self)

        # End of 1. loading stage, all entities of the DXF file are
        # stored in the entity database.

        # Create sections:
        self.classes = ClassesSection(self, sections.get('CLASSES', None))
        self.tables = TablesSection(self, sections.get('TABLES', None))

        # Create *Model_Space and *Paper_Space BLOCK_RECORDS
        # BlockSection setup takes care about the rest:
        self._create_required_block_records()

        # At this point all table entries are required:
        self.blocks = BlocksSection(self, sections.get('BLOCKS', None))
        self.entities = EntitySection(self, sections.get('ENTITIES', None))
        self.objects = ObjectsSection(self, sections.get('OBJECTS', None))

        # only DXF R2013+
        self.acdsdata = AcDsDataSection(self, sections.get('ACDSDATA', None))

        # Store unmanaged sections as raw tags:
        for name, data in sections.items():
            if name not in const.MANAGED_SECTIONS:
                self.stored_sections.append(StoredSection(data))

        # Objects section is not initialized!
        self._2nd_loading_stage()

        # DXF version upgrades:
        if self.dxfversion < DXF12:
            logger.info('DXF version upgrade to DXF R12.')
            self.dxfversion = DXF12

        if self.dxfversion == DXF12:
            self.tables.create_table_handles()

        if self.dxfversion in (DXF13, DXF14):
            logger.info('DXF version upgrade to DXF R2000.')
            self.dxfversion = DXF2000
            self.create_all_arrow_blocks()

        # Objects section setup:
        self.rootdict = self.objects.rootdict
        # Create missing management tables (DICTIONARY):
        self.objects.setup_objects_management_tables(self.rootdict)

        # Setup modelspace- and paperspace layouts:
        self.layouts = Layouts.load(self)

        # Additional work is common to the new and load process:
        self.is_loading = False
        self._finalize_setup()

    def _2nd_loading_stage(self):
        """ Load additional resources from entity database into DXF entities.

        e.g. convert handles into DXFEntity() objects

        """
        db = self.entitydb
        for entity in db.values():
            # The post_load_hook() can return a callable, which should be
            # executed, when the DXF document is fully initialized.
            cmd = entity.post_load_hook(self)
            if cmd is not None:
                self._post_init_commands.append(cmd)

    def create_all_arrow_blocks(self):
        """ For upgrading DXF R12/13/14 files to R2000, it is necessary to
        create all used arrow blocks before saving the DXF file, else $HANDSEED
        is not the next available handle, which is a problem for AutoCAD.
        To be save create all known AutoCAD arrows, because references to arrow
        blocks can be in DIMSTYLE, DIMENSION override, LEADER override and maybe
        other places.

        """
        from ezdxf.render.arrows import ARROWS
        for arrow_name in ARROWS.__acad__:
            ARROWS.create_block(self.blocks, arrow_name)

    def _create_required_block_records(self):
        if '*Model_Space' not in self.block_records:
            self.block_records.new('*Model_Space')
        if '*Paper_Space' not in self.block_records:
            self.block_records.new('*Paper_Space')

    def saveas(self,
               filename: Union[str, 'Path'],
               encoding: str = None,
               fmt: str = 'asc') -> None:
        """ Set :class:`Drawing` attribute :attr:`filename` to `filename` and
        write drawing to the file system. Override file encoding by argument
        `encoding`, handle with care, but this option allows you to create DXF
        files for applications that handles file encoding different than
        AutoCAD.

        Args:
            filename: file name as string
            encoding: override default encoding as Python encoding string like ``'utf-8'``
            fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for Binary DXF

        """
        self.filename = str(filename)
        self.save(encoding=encoding, fmt=fmt)

    def save(self, encoding: str = None, fmt: str = 'asc') -> None:
        """ Write drawing to file-system by using the :attr:`filename` attribute
        as filename. Override file encoding by argument `encoding`, handle with
        care, but this option allows you to create DXF files for applications
        that handles file encoding different than AutoCAD.

        Args:
            encoding: override default encoding as Python encoding string like ``'utf-8'``
            fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for Binary DXF

        """
        # DXF R12, R2000, R2004 - ASCII encoding
        # DXF R2007 and newer - UTF-8 encoding
        # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode
        # characters.

        if encoding is None:
            enc = self.output_encoding
        else:
            # override default encoding, for applications that handle encoding
            # different than AutoCAD
            enc = encoding

        if fmt.startswith('asc'):
            fp = io.open(self.filename,
                         mode='wt',
                         encoding=enc,
                         errors='dxfreplace')
        elif fmt.startswith('bin'):
            fp = open(self.filename, 'wb')
        else:
            raise ValueError(f"Unknown output format: '{fmt}'.")
        try:
            self.write(fp, fmt=fmt)
        finally:
            fp.close()

    def encode(self, s: str) -> bytes:
        """ Encode string `s` with correct encoding and error handler. """
        return s.encode(encoding=self.output_encoding, errors='dxfreplace')

    def write(self, stream: Union[TextIO, BinaryIO], fmt: str = 'asc') -> None:
        """ Write drawing as ASCII DXF to a text stream or as Binary DXF to a
        binary stream. For DXF R2004 (AC1018) and prior open stream with
        drawing :attr:`encoding` and :code:`mode='wt'`. For DXF R2007 (AC1021)
        and later use :code:`encoding='utf-8'`, or better use the later added
        :class:`Drawing` property :attr:`output_encoding` which returns the
        correct encoding automatically. The correct and required error handler
        is :code:`errors='dxfreplace'`!

        If writing to a :class:`StringIO` stream, use :meth:`Drawing.encode` to
        encode the result string from :meth:`StringIO.get_value`::

            binary = doc.encode(stream.get_value())

        Args:
            stream: output text stream or binary stream
            fmt: ``'asc'`` for ASCII DXF (default) or ``'bin'`` for binary DXF

        """
        dxfversion = self.dxfversion
        if dxfversion == DXF12:
            handles = bool(self.header.get('$HANDLING', 0))
        else:
            handles = True
        if dxfversion > DXF12:
            self.classes.add_required_classes(dxfversion)

        self._create_appids()
        self._update_header_vars()
        self.update_extents()
        self.update_limits()
        self._update_metadata()

        if fmt.startswith('asc'):
            tagwriter = TagWriter(stream,
                                  write_handles=handles,
                                  dxfversion=dxfversion)
        elif fmt.startswith('bin'):
            tagwriter = BinaryTagWriter(
                stream,
                write_handles=handles,
                dxfversion=dxfversion,
                encoding=self.output_encoding,
            )
            tagwriter.write_signature()
        else:
            raise ValueError(f"Unknown output format: '{fmt}'.")

        self.export_sections(tagwriter)

    def encode_base64(self) -> bytes:
        """ Returns DXF document as base64 encoded binary data. """
        stream = io.StringIO()
        self.write(stream)
        # Create binary data:
        binary_data = self.encode(stream.getvalue())
        # Create Windows line endings and do base64 encoding:
        return base64.encodebytes(binary_data.replace(b'\n', b'\r\n'))

    def export_sections(self, tagwriter: 'TagWriter') -> None:
        """ DXF export sections. (internal API) """
        dxfversion = tagwriter.dxfversion
        self.header.export_dxf(tagwriter)
        if dxfversion > DXF12:
            self.classes.export_dxf(tagwriter)
        self.tables.export_dxf(tagwriter)
        self.blocks.export_dxf(tagwriter)
        self.entities.export_dxf(tagwriter)
        if dxfversion > DXF12:
            self.objects.export_dxf(tagwriter)
        if self.acdsdata.is_valid:
            self.acdsdata.export_dxf(tagwriter)
        for section in self.stored_sections:
            section.export_dxf(tagwriter)

        tagwriter.write_tag2(0, 'EOF')

    def update_extents(self):
        msp = self.modelspace()
        self.header['$EXTMIN'] = msp.dxf.extmin
        self.header['$EXTMAX'] = msp.dxf.extmax
        active_layout = self.active_layout()
        self.header['$PEXTMIN'] = active_layout.dxf.extmin
        self.header['$PEXTMAX'] = active_layout.dxf.extmax

    def update_limits(self):
        msp = self.modelspace()
        self.header['$LIMMIN'] = msp.dxf.limmin
        self.header['$LIMMAX'] = msp.dxf.limmax
        active_layout = self.active_layout()
        self.header['$PLIMMIN'] = active_layout.dxf.limmin
        self.header['$PLIMMAX'] = active_layout.dxf.limmax

    def _update_header_vars(self):
        from ezdxf.lldxf.const import acad_maint_ver

        # set or correct $CMATERIAL handle
        material = self.entitydb.get(self.header.get('$CMATERIAL', None))
        if material is None or material.dxftype() != 'MATERIAL':
            if 'ByLayer' in self.materials:
                self.header['$CMATERIAL'] = self.materials.get(
                    'ByLayer').dxf.handle
            else:  # set any handle, except '0' which crashes BricsCAD
                self.header['$CMATERIAL'] = '45'

        # set ACAD maintenance version - same values as used by BricsCAD
        self.header['$ACADMAINTVER'] = acad_maint_ver.get(self.dxfversion, 0)

    def _update_metadata(self):
        if options.write_fixed_meta_data_for_testing:
            fixed_date = juliandate(datetime(2000, 1, 1, 0, 0))
            self.header['$TDCREATE'] = fixed_date
            self.header['$TDUCREATE'] = fixed_date
            self.header['$TDUPDATE'] = fixed_date
            self.header['$TDUUPDATE'] = fixed_date
            self.header[
                '$VERSIONGUID'] = '00000000-0000-0000-0000-000000000000'
            self.header[
                '$FINGERPRINTGUID'] = '00000000-0000-0000-0000-000000000000'
        else:
            now = datetime.now()
            self.header['$TDUPDATE'] = juliandate(now)
            self.reset_version_guid()

        self.header['$HANDSEED'] = str(self.entitydb.handles)  # next handle
        self.header['$DWGCODEPAGE'] = tocodepage(self.encoding)

    def _create_appid_if_not_exist(self, name: str, flags: int = 0) -> None:
        if name not in self.appids:
            self.appids.new(name, {'flags': flags})

    def _create_appids(self):
        self._create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0)

    @property
    def acad_release(self) -> str:
        """ Returns the AutoCAD release number like ``'R12'`` or ``'R2000'``.
        """
        return const.acad_release.get(self.dxfversion, "unknown")

    @property
    def layers(self) -> 'Table':
        return self.tables.layers

    @property
    def linetypes(self) -> 'Table':
        return self.tables.linetypes

    @property
    def styles(self) -> 'Table':
        return self.tables.styles

    @property
    def dimstyles(self) -> 'Table':
        return self.tables.dimstyles

    @property
    def ucs(self) -> 'Table':
        return self.tables.ucs

    @property
    def appids(self) -> 'Table':
        return self.tables.appids

    @property
    def views(self) -> 'Table':
        return self.tables.views

    @property
    def block_records(self) -> 'Table':
        return self.tables.block_records

    @property
    def viewports(self) -> 'ViewportTable':
        return self.tables.viewports

    @property
    def plotstyles(self) -> 'Dictionary':
        return self.rootdict['ACAD_PLOTSTYLENAME']

    @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) -> 'Modelspace':
        """ Returns the modelspace layout, displayed as ``'Model'`` tab in CAD
        applications, defined by block record named ``'*Model_Space'``.
        """
        return self.layouts.modelspace()

    def layout(self, name: str = None) -> 'Layout':
        """ Returns paperspace layout `name` or returns first layout in tab
        order if `name` is ``None``.
        """
        return self.layouts.get(name)

    def active_layout(self) -> 'Layout':
        """ Returns the active paperspace layout, defined by block record
        name ``'*Paper_Space'``.
        """
        return self.layouts.active_layout()

    def layout_names(self) -> Iterable[str]:
        """ Returns all layout names (modelspace ``'Model'`` included) in
        arbitrary order.
        """
        return list(self.layouts.names())

    def layout_names_in_taborder(self) -> Iterable[str]:
        """ Returns all layout names (modelspace included, always first name)
        in tab order.
        """
        return list(self.layouts.names_in_taborder())

    def reset_fingerprint_guid(self):
        """ Reset fingerprint GUID. """
        self.header['$FINGERPRINTGUID'] = guid()

    def reset_version_guid(self):
        """ Reset version GUID. """
        self.header['$VERSIONGUID'] = guid()

    @property
    def acad_compatible(self) -> bool:
        """ Returns ``True`` if drawing is AutoCAD compatible. """
        return self._acad_compatible

    def add_acad_incompatibility_message(self, msg: str):
        """ Add AutoCAD incompatibility message. (internal API) """
        self._acad_compatible = False
        if msg not in self._acad_incompatibility_reason:
            self._acad_incompatibility_reason.add(msg)
            logger.warning(
                f'Drawing is incompatible to AutoCAD, because {msg}.')

    def query(self, query: str = '*') -> EntityQuery:
        """
        Entity query over all layouts and blocks, excluding the OBJECTS section.

        Args:
            query: query string

        .. seealso::

            :ref:`entity query string` and :ref:`entity queries`

        """
        return EntityQuery(self.chain_layouts_and_blocks(), query)

    def groupby(self, dxfattrib="", key=None) -> dict:
        """ Groups DXF entities of all layouts and blocks (excluding the
        OBJECTS section) by a DXF attribute or a key function.

        Args:
            dxfattrib: grouping DXF attribute like ``'layer'``
            key: key function, which accepts a :class:`DXFEntity` as argument
                and returns a hashable grouping key or ``None`` to ignore
                this entity.

        .. seealso::

            :func:`~ezdxf.groupby.groupby` documentation

        """
        return groupby(self.chain_layouts_and_blocks(), dxfattrib, key)

    def chain_layouts_and_blocks(self) -> Iterable['DXFEntity']:
        """ Chain entity spaces of all layouts and blocks. Yields an iterator
        for all entities in all layouts and blocks.

        """
        layouts = list(self.layouts_and_blocks())
        return chain.from_iterable(layouts)

    def layouts_and_blocks(self) -> Iterable['GenericLayoutType']:
        """ Iterate over all layouts (modelspace and paperspace) and all
        block definitions.

        """
        return iter(self.blocks)

    def delete_layout(self, name: str) -> None:
        """
        Delete paper space layout `name` and all entities owned by this layout.
        Available only for DXF R2000 or later, DXF R12 supports only one
        paperspace and it can't be deleted.

        """
        if name not in self.layouts:
            raise const.DXFValueError(f"Layout '{name}' does not exist.")
        else:
            self.layouts.delete(name)

    def new_layout(self, name, dxfattribs=None) -> 'Layout':
        """
        Create a new paperspace layout `name`. Returns a
        :class:`~ezdxf.layouts.Layout` object.
        DXF R12 (AC1009) supports only one paperspace layout, only the active
        paperspace layout is saved, other layouts are dismissed.

        Args:
            name: unique layout name
            dxfattribs: additional DXF attributes for the
                :class:`~ezdxf.entities.layout.DXFLayout` entity

        Raises:
            DXFValueError: :class:`~ezdxf.layouts.Layout` `name` already exist

        """
        if name in self.layouts:
            raise const.DXFValueError(f"Layout '{name}' already exists.")
        else:
            return self.layouts.new(name, dxfattribs)

    def acquire_arrow(self, name: str):
        """ For standard AutoCAD and ezdxf arrows create block definitions if
        required, otherwise check if block `name` exist. (internal API)

        """
        from ezdxf.render.arrows import ARROWS
        if ARROWS.is_acad_arrow(name) or ARROWS.is_ezdxf_arrow(name):
            ARROWS.create_block(self.blocks, name)
        elif name not in self.blocks:
            raise const.DXFValueError(f'Arrow block "{name}" does not exist.')

    def add_image_def(self,
                      filename: str,
                      size_in_pixel: Tuple[int, int],
                      name=None):
        """ Add an image definition to the objects section.

        Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing
        (objects section). `filename` is the image file name as relative or
        absolute path and `size_in_pixel` is the image size in pixel as (x, y)
        tuple. To avoid dependencies to external packages, `ezdxf` can not
        determine the image size by itself. Returns a
        :class:`~ezdxf.entities.image.ImageDef` entity which is needed to
        create an image reference. `name` is the internal image name, if set to
        ``None``, name is auto-generated.

        Absolute image paths works best for AutoCAD but not really good, you
        have to update external references manually in AutoCAD, which is not
        possible in TrueView. If the drawing units differ from 1 meter, you
        also have to use: :meth:`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)

        .. seealso::

            :ref:`tut_image`

        """
        if 'ACAD_IMAGE_VARS' not in self.rootdict:
            self.objects.set_raster_variables(frame=0, quality=1, units='m')
        if name is None:
            name = filename
        return self.objects.add_image_def(filename, size_in_pixel, name)

    def set_raster_variables(self,
                             frame: int = 0,
                             quality: int = 1,
                             units: str = '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 defines the real world unit
                for one drawing unit 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
                ===== ===========================

        """
        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)
        var_dict = self.rootdict.get_required_dict('AcDbVariableDictionary')
        var_dict.set_or_add_dict_var('WIPEOUTFRAME', str(frame))

    def add_underlay_def(self,
                         filename: str,
                         format: str = 'ext',
                         name: str = None):
        """ Add an :class:`~ezdxf.entities.underlay.UnderlayDef` entity to the
        drawing (OBJECTS section).
        `filename` is the underlay file name as relative or absolute path and
        `format` as string (pdf, dwf, dgn).
        The underlay definition is required to create an underlay reference.

        Args:
            filename: underlay file name
            format: file format as string ``'pdf'|'dwf'|'dgn'`` or ``'ext'``
                for getting file format from filename extension
            name: pdf format = page number to display; dgn format =
                ``'default'``; dwf: ????

        .. seealso::

            :ref:`tut_underlay`

        """
        if format == 'ext':
            format = filename[-3:]
        return self.objects.add_underlay_def(filename, format, name)

    def add_xref_def(self,
                     filename: str,
                     name: str,
                     flags: int = BLK_XREF | BLK_EXTERNAL):
        """
        Add an external reference (xref) definition to the blocks section.

        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 audit(self) -> 'Auditor':
        """ Checks document integrity and fixes all fixable problems, not
        fixable problems are stored in :attr:`Auditor.errors`.

        If you are messing around with internal structures, call this method
        before saving to be sure to export valid DXF documents, but be aware
        this is a long running task.

        """
        from ezdxf.audit import Auditor
        auditor = Auditor(self)
        auditor.run()
        return auditor

    def validate(self, print_report=True) -> bool:
        """ Simple way to run an audit process. Fixes all fixable problems,
        return ``False`` if not fixable errors occurs, to get more information
        about not fixable errors use :meth:`audit` method instead.

        Args:
            print_report: print report to stdout

        Returns: ``True`` if no errors occurred

        """
        auditor = self.audit()
        if len(auditor):
            if print_report:
                auditor.print_error_report()
            return False
        else:
            return True

    def set_modelspace_vport(self, height, center=(0, 0)) -> 'VPort':
        r""" Set initial view/zoom location for the modelspace, this replaces
        the current "\*Active" viewport configuration.

        Args:
             height: modelspace area to view
             center: modelspace location to view in the center of the CAD
                application window.

        """
        self.viewports.delete_config('*Active')
        vport = cast('VPort', self.viewports.new('*Active'))
        vport.dxf.center = center
        vport.dxf.height = height
        return vport
コード例 #6
0
class Drawing:
    def __init__(self, dxfversion=DXF2013):
        self.entitydb = EntityDB()
        self.dxffactory = EntityFactory(self)
        self.tracker = Tracker()
        target_dxfversion = dxfversion.upper()
        self._dxfversion = acad_release_to_dxf_version.get(
            target_dxfversion, target_dxfversion)
        if self._dxfversion not in versions_supported_by_new:
            raise DXFVersionError('Unsupported DXF version "{}".'.format(
                self.dxfversion))
        self._loaded_dxfversion = None  # if loaded from file, store original dxf version
        self.encoding = 'cp1252'
        self.filename = None  # type: str # read/write

        # named objects dictionary
        self.rootdict = None  # type: Dictionary

        # DXF sections
        self.header = None  # type: HeaderSection
        self.classes = None  # type: ClassesSection
        self.tables = None  # type: TablesSection
        self.blocks = None  # type: BlocksSection
        self.entities = None  # type: EntitySection
        self.objects = None  # type: ObjectsSection

        # DXF R2013 and later
        self.acdsdata = None  # type: AcDsDataSection

        self.stored_sections = []
        self.layouts = None  # type: Layouts
        self.groups = None  # type: GroupCollection  # read only
        self.materials = None  # type: MaterialCollection # read only
        self.mleader_styles = None  # type: MLeaderStyleCollection # read only
        self.mline_styles = None  # type: MLineStyleCollection # read only
        self._acad_compatible = True  # will generated DXF file compatible with AutoCAD
        self._dimension_renderer = DimensionRenderer(
        )  # set DIMENSION rendering engine
        self._acad_incompatibility_reason = set(
        )  # avoid multiple warnings for same reason
        # Don't create any new entities here:
        # New created handles could collide with handles loaded from DXF file.
        assert len(self.entitydb) == 0

    @classmethod
    def new(cls, dxfversion: str = DXF2013) -> 'Drawing':
        """ Create new drawing. Package users should use the factory function :func:`ezdxf.new`.
        (internal API)
        """
        doc = Drawing(dxfversion)
        doc._setup()
        return doc

    def _setup(self):
        self.header = HeaderSection.new()
        self.classes = ClassesSection(self)
        self.tables = TablesSection(self)
        self.blocks = BlocksSection(self)
        self.entities = EntitySection(self)
        self.objects = ObjectsSection(self)
        self.acdsdata = AcDsDataSection(
            self)  # AcDSData section is not supported for new drawings
        self.rootdict = self.objects.rootdict
        self.objects.setup_objects_management_tables(
            self.rootdict)  # create missing tables
        self.layouts = Layouts.setup(self)
        self._finalize_setup()

    def _finalize_setup(self):
        """ Common setup tasks for new and loaded DXF drawings. """
        self.groups = GroupCollection(self)
        self.materials = MaterialCollection(self)

        self.mline_styles = MLineStyleCollection(self)
        # all required internal structures are ready
        # now do the stuff to please AutoCAD
        self._create_required_table_entries()

        # mleader_styles requires text styles
        self.mleader_styles = MLeaderStyleCollection(self)
        self._set_required_layer_attributes()
        self._setup_metadata()

    def _create_required_table_entries(self):
        self._create_required_vports()
        self._create_required_linetypes()
        self._create_required_layers()
        self._create_required_styles()
        self._create_required_appids()
        self._create_required_dimstyles()

    def _set_required_layer_attributes(self):
        for layer in self.layers:  # type: Layer
            layer.set_required_attributes()

    def _create_required_vports(self):
        if '*Active' not in self.viewports:
            self.viewports.new('*Active')

    def _create_required_appids(self):
        if 'ACAD' not in self.appids:
            self.appids.new('ACAD')

    def _create_required_linetypes(self):
        linetypes = self.linetypes
        for name in ('ByBlock', 'ByLayer', 'Continuous'):
            if name not in linetypes:
                linetypes.new(name)

    def _create_required_dimstyles(self):
        if 'Standard' not in self.dimstyles:
            self.dimstyles.new('Standard')

    def _create_required_styles(self):
        if 'Standard' not in self.styles:
            self.styles.new('Standard')

    def _create_required_layers(self):
        layers = self.layers
        if '0' not in layers:
            layers.new('0')
        if 'Defpoints' not in layers:
            layers.new('Defpoints', dxfattribs={'plot': 0})  # do not plot

    def _setup_metadata(self):
        self.header['$ACADVER'] = self.dxfversion
        self.header['$TDCREATE'] = juliandate(datetime.now())
        self.reset_fingerprint_guid()
        self.reset_version_guid()

    @property
    def dxfversion(self) -> str:
        """ Get current DXF version. """
        return self._dxfversion

    @dxfversion.setter
    def dxfversion(self, version) -> None:
        """ Set current DXF version. """
        self._dxfversion = self._validate_dxf_version(version)
        self.header['$ACADVER'] = version

    @property
    def output_encoding(self):
        """ Returns required output encoding for writing document to a text streams. """
        return 'utf-8' if self.dxfversion >= DXF2007 else self.encoding

    def _validate_dxf_version(self, version: str) -> str:
        version = version.upper()
        version = acad_release_to_dxf_version.get(
            version, version)  # translates 'R12' -> 'AC1009'
        if version not in versions_supported_by_save:
            raise DXFVersionError(
                'Unsupported DXF version "{}".'.format(version))
        if version == DXF12:
            if self._dxfversion > DXF12:
                logger.warning(
                    'Downgrade from DXF {} to R12 may create an invalid DXF file.'
                    .format(self.acad_release))
        elif version < self._dxfversion:
            logger.info(
                'Downgrade from DXF {} to {} can cause lost of features.'.
                format(self.acad_release, acad_release[version]))
        return version

    @classmethod
    def read(cls,
             stream: TextIO,
             legacy_mode: bool = False,
             filter_stack: TFilterStack = None) -> 'Drawing':
        """ Open an existing drawing. Package users should use the factory function :func:`ezdxf.read`.

        Args:
             stream: text stream yielding text (unicode) strings by readline()
             legacy_mode: apply some low level filters to correct some quirks allowed in legacy (R12) files
             filter_stack: interface to put filters between reading layers, list of callable filters, for now
                           two levels are supported, after low level tagging (DXFVertex) and after compiling tags to
                           DXFVertex and DXFBinaryTag.

                TFilterStack: Sequence[Sequence[Callable[[Iterable[DXFTag]], Iterable[DXFTag]]]]
                e.g. [(raw_tag_filter1, raw_tag_filter2), (compiled_tag_filter1, )]

        (internal API)
        """
        from .lldxf.tagger import low_level_tagger, tag_compiler
        raw_tag_filters = []
        compiled_tag_filters = []

        if filter_stack:
            # maybe more levels in the future
            raw_tag_filters, compiled_tag_filters, *_ = filter_stack

        # legacy mode overrides filter_stack
        if legacy_mode:
            raw_tag_filters = [
                repair.tag_reorder_layer, repair.filter_invalid_yz_point_codes
            ]
            compiled_tag_filters = []

        # low level tag compiler, creates simple tuple like tags DXFTag(group code, value)
        tagger = low_level_tagger(stream)

        # apply low level filters
        for _filter in raw_tag_filters:
            tagger = _filter(tagger)

        # compiles vertices and binary tags into DXFVertex() or DXFBinaryTag()
        tagger = tag_compiler(tagger)

        # apply compiled tags filter
        for _filter in compiled_tag_filters:
            tagger = _filter(tagger)

        doc = Drawing()
        doc._load(tagger)
        return doc

    @classmethod
    def from_tags(cls, compiled_tags: Iterable['DXFTag']) -> 'Drawing':
        """ Create new drawing from compiled tags. (internal API)"""
        doc = Drawing()
        doc._load(compiled_tags)
        return doc

    def _load(self, tagger: Iterable['DXFTag']):
        sections = load_dxf_structure(
            tagger)  # load complete DXF entity structure
        try:  # discard section THUMBNAILIMAGE
            del sections['THUMBNAILIMAGE']
        except KeyError:
            pass
        # -----------------------------------------------------------------------------------
        # create header section:
        # all header tags are the first DXF structure entity
        header_entities = sections.get('HEADER', [None])[0]
        if header_entities is None:
            # create default header, files without header are by default DXF R12
            self.header = HeaderSection.new(dxfversion=DXF12)
        else:
            self.header = HeaderSection.load(header_entities)
        # -----------------------------------------------------------------------------------
        # missing $ACADVER defaults to DXF R12
        self._dxfversion = self.header.get('$ACADVER', DXF12)  # type: str
        self._loaded_dxfversion = self._dxfversion  # save dxf version of loaded file
        self.encoding = toencoding(self.header.get(
            '$DWGCODEPAGE', 'ANSI_1252'))  # type: str # read/write
        # get handle seed
        seed = self.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(sections, self.dxffactory)
        # all handles used in the DXF file are known at this point
        # -----------------------------------------------------------------------------------
        # create sections:
        self.classes = ClassesSection(self, sections.get('CLASSES', None))
        self.tables = TablesSection(self, sections.get('TABLES', None))
        # create *Model_Space and *Paper_Space BLOCK_RECORDS
        # BlockSection setup takes care about the rest
        self._create_required_block_records()
        # table records available
        self.blocks = BlocksSection(self, sections.get('BLOCKS', None))

        self.entities = EntitySection(self, sections.get('ENTITIES', None))
        self.objects = ObjectsSection(self, sections.get('OBJECTS', None))
        # only valid for DXF R2013 and later
        self.acdsdata = AcDsDataSection(self, sections.get('ACDSDATA', None))

        for name, data in sections.items():
            if name not in MANAGED_SECTIONS:
                self.stored_sections.append(StoredSection(data))
        # -----------------------------------------------------------------------------------
        if self.dxfversion < DXF12:
            # upgrade to DXF R12
            logger.info('Upgrading drawing to DXF R12.')
            self.dxfversion = DXF12

        # DIMSTYLE: ezdxf uses names for blocks, linetypes and text style as internal data, handles are set at export
        # requires BLOCKS and TABLES section!
        self.tables.resolve_dimstyle_names()

        if self.dxfversion == DXF12:
            # TABLE requires in DXF12 no handle and has no owner tag, but DXF R2000+, requires a TABLE with handle
            # and each table entry has an owner tag, pointing to the TABLE entry
            self.tables.create_table_handles()

        if self.dxfversion in (DXF13, DXF14):
            # upgrade to DXF R2000
            self.dxfversion = DXF2000

        self.rootdict = self.objects.rootdict
        self.objects.setup_objects_management_tables(
            self.rootdict)  # create missing tables

        self.layouts = Layouts.load(self)
        self._finalize_setup()

    def _create_required_block_records(self):
        if '*Model_Space' not in self.block_records:
            self.block_records.new('*Model_Space')
        if '*Paper_Space' not in self.block_records:
            self.block_records.new('*Paper_Space')

    def saveas(self, filename: str, encoding: str = None) -> None:
        """
        Set :class:`Drawing` attribute :attr:`filename` to `filename` and write drawing to the file system.
        Override file encoding by argument `encoding`, handle with care, but this option allows you to create
        DXF files for applications that handles file encoding different than AutoCAD.

        Args:
            filename: file name as string
            encoding: override default encoding as Python encoding string like ``'utf-8'``

        """
        self.filename = filename
        self.save(encoding=encoding)

    def save(self, encoding: str = None) -> None:
        """
        Write drawing to file-system by using the :attr:`filename` attribute as filename.
        Override file encoding by argument `encoding`, handle with care, but this option allows you to create
        DXF files for applications that handles file encoding different than AutoCAD.

        Args:
            encoding: override default encoding as Python encoding string like ``'utf-8'``

        """
        # DXF R12, R2000, R2004 - ASCII encoding
        # DXF R2007 and newer - UTF-8 encoding
        # in ASCII mode, unknown characters will be escaped as \U+nnnn unicode characters.

        if encoding is None:
            enc = self.output_encoding
        else:  # override default encoding, for applications that handles encoding different than AutoCAD
            enc = encoding

        with io.open(self.filename,
                     mode='wt',
                     encoding=enc,
                     errors='dxfreplace') as fp:
            self.write(fp)

    def write(self, stream: TextIO) -> None:
        """
        Write drawing to a text stream. For DXF R2004 (AC1018) and prior open stream with drawing
        :attr:`encoding` and :code:`mode='wt'`. For DXF R2007 (AC1021) and later use
        :code:`encoding='utf-8'`, or better use the later added :class:`Drawing` property :attr:`output_encoding`
        which returns the correct encoding automatically.

        Args:
            stream: output text stream

        """
        dxfversion = self.dxfversion
        if dxfversion == DXF12:
            handles = bool(self.header.get('$HANDLING', 0))
        else:
            handles = True
        if dxfversion > DXF12:
            self.classes.add_required_classes(dxfversion)

        self._create_appids()
        self._update_header_vars()
        self._update_metadata()
        tagwriter = TagWriter(stream,
                              write_handles=handles,
                              dxfversion=dxfversion)
        self.export_sections(tagwriter)

    def export_sections(self, tagwriter: 'TagWriter') -> None:
        """ DXF export sections. (internal API) """
        dxfversion = tagwriter.dxfversion
        self.header.export_dxf(tagwriter)
        if dxfversion > DXF12:
            self.classes.export_dxf(tagwriter)
        self.tables.export_dxf(tagwriter)
        self.blocks.export_dxf(tagwriter)
        self.entities.export_dxf(tagwriter)
        if dxfversion > DXF12:
            self.objects.export_dxf(tagwriter)
        if self.acdsdata.is_valid:
            self.acdsdata.export_dxf(tagwriter)
        for section in self.stored_sections:
            section.export_dxf(tagwriter)

        tagwriter.write_tag2(0, 'EOF')

    def _update_header_vars(self):
        from ezdxf.lldxf.const import acad_maint_ver

        # set or correct $CMATERIAL handle
        material = self.entitydb.get(self.header.get('$CMATERIAL', None))
        if material is None or material.dxftype() != 'MATERIAL':
            if 'ByLayer' in self.materials:
                self.header['$CMATERIAL'] = self.materials.get(
                    'ByLayer').dxf.handle
            else:  # set any handle, except '0' which crashes BricsCAD
                self.header['$CMATERIAL'] = '45'

        # set ACAD maintenance version - same values as used by BricsCAD
        self.header['$ACADMAINTVER'] = acad_maint_ver.get(self.dxfversion, 0)

    def _update_metadata(self):
        now = datetime.now()
        self.header['$TDUPDATE'] = juliandate(now)
        self.header['$HANDSEED'] = str(self.entitydb.next_handle())
        self.header['$DWGCODEPAGE'] = tocodepage(self.encoding)
        self.reset_version_guid()

    def _create_appid_if_not_exist(self, name: str, flags: int = 0) -> None:
        if name not in self.appids:
            self.appids.new(name, {'flags': flags})

    def _create_appids(self):
        if 'HATCH' in self.tracker.dxftypes:
            self._create_appid_if_not_exist('HATCHBACKGROUNDCOLOR', 0)

    @property
    def acad_release(self) -> str:
        """ Returns the AutoCAD release number like ``'R12'`` or ``'R2000'``. """
        return acad_release.get(self.dxfversion, "unknown")

    @property
    def layers(self) -> 'Table':
        return self.tables.layers

    @property
    def linetypes(self) -> 'Table':
        return self.tables.linetypes

    @property
    def styles(self) -> 'Table':
        return self.tables.styles

    @property
    def dimstyles(self) -> 'Table':
        return self.tables.dimstyles

    @property
    def ucs(self) -> 'Table':
        return self.tables.ucs

    @property
    def appids(self) -> 'Table':
        return self.tables.appids

    @property
    def views(self) -> 'Table':
        return self.tables.views

    @property
    def block_records(self) -> 'Table':
        return self.tables.block_records

    @property
    def viewports(self) -> 'ViewportTable':
        return self.tables.viewports

    @property
    def plotstyles(self) -> 'Dictionary':
        return self.rootdict['ACAD_PLOTSTYLENAME']

    @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) -> 'Layout':
        """ Returns the modelspace layout, displayed as ``'Model'`` tab in CAD applications, defined by block record
        named ``'*Model_Space'``.
        """
        return self.layouts.modelspace()

    def layout(self, name: str = None) -> 'Layout':
        """ Returns paperspace layout `name` or returns first layout in tab order if `name` is ``None``. """
        return self.layouts.get(name)

    def active_layout(self) -> 'Layout':
        """ Returns the active paperspace layout, defined by block record name ``'*Paper_Space'``. """
        return self.layouts.active_layout()

    def layout_names(self) -> Iterable[str]:
        """ Returns all layout names (modelspace ``'Model'`` included) in arbitrary order. """
        return list(self.layouts.names())

    def layout_names_in_taborder(self) -> Iterable[str]:
        """ Returns all layout names (modelspace included, always first name) in tab order. """
        return list(self.layouts.names_in_taborder())

    def reset_fingerprint_guid(self):
        """ Reset fingerprint GUID. """
        self.header['$FINGERPRINTGUID'] = guid()

    def reset_version_guid(self):
        """ Reset version GUID. """
        self.header['$VERSIONGUID'] = guid()

    @property
    def acad_compatible(self) -> bool:
        """ Returns ``True`` if drawing is AutoCAD compatible. """
        return self._acad_compatible

    def add_acad_incompatibility_message(self, msg: str):
        """ Add AutoCAD incompatibility message. (internal API) """
        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))

    def query(self, query: str = '*') -> EntityQuery:
        """
        Entity query over all layouts and blocks, excluding the OBJECTS section.

        Args:
            query: query string

        .. seealso::

            :ref:`entity query string` and :ref:`entity queries`

        """
        return EntityQuery(self.chain_layouts_and_blocks(), query)

    def groupby(self, dxfattrib="", key=None) -> dict:
        """
        Groups DXF entities of all layouts and blocks (excluding the OBJECTS section) by a DXF attribute or a key
        function.

        Args:
            dxfattrib: grouping DXF attribute like ``'layer'``
            key: key function, which accepts a :class:`DXFEntity` as argument and returns a hashable grouping key
                 or ``None`` to ignore this entity.

        .. seealso::

            :func:`~ezdxf.groupby.groupby` documentation

        """
        return groupby(self.chain_layouts_and_blocks(), dxfattrib, key)

    def chain_layouts_and_blocks(self) -> Iterable['DXFEntity']:
        """
        Chain entity spaces of all layouts and blocks. Yields an iterator for all entities in all layouts and blocks.

        """
        layouts = list(self.layouts_and_blocks())
        return chain.from_iterable(layouts)

    def layouts_and_blocks(self) -> Iterable['LayoutType']:
        """
        Iterate over all layouts (modelspace and paperspace) and all block definitions.

        """
        return iter(self.blocks)

    def delete_layout(self, name: str) -> None:
        """
        Delete paper space layout `name` and all entities owned by this layout. Available only for DXF R2000 or later,
        DXF R12 supports only one paperspace and it can't be deleted.

        """
        if name not in self.layouts:
            raise DXFValueError("Layout '{}' does not exist.".format(name))
        else:
            self.layouts.delete(name)

    def new_layout(self, name, dxfattribs=None) -> 'Layout':
        """
        Create a new paperspace layout `name`. Returns a :class:`~ezdxf.layouts.Layout` object.
        DXF R12 (AC1009) supports only one paperspace layout, only the active paperspace layout is saved, other layouts
        are dismissed.

        Args:
            name: unique layout name
            dxfattribs: additional DXF attributes for the :class:`~ezdxf.entities.layout.DXFLayout` entity

        Raises:
            DXFValueError: :class:`~ezdxf.layouts.Layout` `name` already exist

        """
        if name in self.layouts:
            raise DXFValueError("Layout '{}' already exists.".format(name))
        else:
            return self.layouts.new(name, dxfattribs)

    def acquire_arrow(self, name: str):
        """
        For standard AutoCAD and ezdxf arrows create block definitions if required, otherwise check if block `name`
        exist. (internal API)

        """
        from ezdxf.render.arrows import ARROWS
        if ARROWS.is_acad_arrow(name) or ARROWS.is_ezdxf_arrow(name):
            ARROWS.create_block(self.blocks, name)
        elif name not in self.blocks:
            raise DXFValueError(
                'Arrow block "{}" does not exist.'.format(name))

    def add_image_def(self,
                      filename: str,
                      size_in_pixel: Tuple[int, int],
                      name=None):
        """
        Add an image definition to the objects section.

        Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing (objects section). `filename` is the image
        file name as relative or absolute path and `size_in_pixel` is the image size in pixel as (x, y) tuple. To avoid
        dependencies to external packages, `ezdxf` can not determine the image size by itself. Returns a
        :class:`~ezdxf.entities.image.ImageDef` entity which is needed to create an image reference. `name` is the
        internal image name, if set to ``None``, name is auto-generated.

        Absolute image paths works best for AutoCAD but not really good, you have to update external references manually
        in AutoCAD, which is not possible in TrueView. If the drawing units differ from 1 meter, you also have to use:
        :meth:`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)

        .. seealso::

            :ref:`tut_image`

        """
        if 'ACAD_IMAGE_VARS' not in self.rootdict:
            self.objects.set_raster_variables(frame=0, quality=1, units='m')
        if name is None:
            name = filename
        return self.objects.add_image_def(filename, size_in_pixel, name)

    def set_raster_variables(self,
                             frame: int = 0,
                             quality: int = 1,
                             units: str = '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 defines the real world unit for one drawing unit 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
                   ===== ===========================

        """
        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: str,
                         format: str = 'ext',
                         name: str = None):
        """
        Add an :class:`~ezdxf.entities.underlay.UnderlayDef` entity to the drawing (OBJECTS section).
        `filename` is the underlay file name as relative or absolute path and `format` as string (pdf, dwf, dgn).
        The underlay definition is required to create an underlay reference.

        Args:
            filename: underlay file name
            format: file format as string ``'pdf'|'dwf'|'dgn'`` or ``'ext'`` for getting file format from filename extension
            name: pdf format = page number to display; dgn format = ``'default'``; dwf: ????

        .. seealso::

            :ref:`tut_underlay`

        """
        if format == 'ext':
            format = filename[-3:]
        return self.objects.add_underlay_def(filename, format, name)

    def add_xref_def(self,
                     filename: str,
                     name: str,
                     flags: int = BLK_XREF | BLK_EXTERNAL):
        """
        Add an external reference (xref) definition to the blocks section.

        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 cleanup(self, groups=True) -> None:
        """
        Cleanup drawing. Call it before saving the drawing but only if necessary, the process could take a while.

        Args:
            groups: 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) -> bool:
        """
        Simple way to run an audit process.

        Args:
            print_report: print report to stdout

        Returns: ``True`` if no errors occurred

        """
        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 set_modelspace_vport(self, height, center=(0, 0)) -> 'VPort':
        """ Set initial view/zoom location for the modelspace, this replaces the actual
        ``'*Active'`` viewport configuration.

        Args:
             height: modelspace area to view
             center: modelspace location to view in the center of the CAD application window.

        .. versionadded:: 0.11

        """
        self.viewports.delete_config('*Active')
        vport = cast('VPort', self.viewports.new('*Active'))
        vport.dxf.center = center
        vport.dxf.height = height
        return vport