# Purpose: repair/setup required DXF structures in existing DXF files (created by other DXF libs) # Created: 05.03.2016 # Copyright (C) 2016, Manfred Moitzi # License: MIT License # --------------------------------------------------- # # Welcome to the place, where it gets dirty and ugly! # # --------------------------------------------------- # from typing import TYPE_CHECKING, Iterable, Optional, List from functools import partial import logging from .extendedtags import ExtendedTags from .tags import DXFTag, Tags from .const import DXFInternalEzdxfError, DXFValueError, DXFKeyError, SUBCLASS_MARKER logger = logging.getLogger('ezdxf') if TYPE_CHECKING: # import forward declarations from ezdxf.eztypes import Drawing def setup_layouts(dwg: 'Drawing'): layout_dict = dwg.rootdict.get_required_dict('ACAD_LAYOUT') if 'Model' not in layout_dict: # do it only if model space is not defined setup_model_space(dwg) setup_paper_space(dwg) def setup_model_space(dwg: 'Drawing'): setup_layout_space(dwg, 'Model', '*Model_Space', _MODEL_SPACE_LAYOUT_TPL) def setup_paper_space(dwg: 'Drawing'): setup_layout_space(dwg, 'Layout1', '*Paper_Space', _PAPER_SPACE_LAYOUT_TPL) def setup_layout_space(dwg: 'Drawing', layout_name: str, block_name: str, tag_string: str): # This is just necessary for existing DXF drawings without properly setup management structures. # Layout structure is not initialized at this runtime phase logger.info('creating LAYOUT structure for: {}'.format(layout_name)) def get_block_record_by_alt_names(names: Iterable[str]): for name in names: try: brecord = dwg.block_records.get(name) except DXFValueError: pass else: return brecord raise DXFKeyError layout_dict = dwg.rootdict.get_required_dict('ACAD_LAYOUT') if layout_name in layout_dict: return try: block_record = get_block_record_by_alt_names((block_name, block_name.upper())) except DXFKeyError: raise NotImplementedError("'%s' block record setup not implemented, send an email to " " with your DXF file." % block_name) real_block_name = block_record.dxf.name # can be *Model_Space or *MODEL_SPACE block_record_handle = block_record.dxf.handle logger.debug('found {}: {}'.format(str(block_record), real_block_name)) try: block_layout = dwg.blocks.get(real_block_name) logger.debug('found {}: {}'.format(str(block_layout.block), real_block_name)) except DXFKeyError: logger.debug('expected BLOCK: {} not found.'.format(real_block_name)) raise NotImplementedError("'%s' block setup not implemented, send an email to " " with your DXF file." % real_block_name) else: block_layout.set_block_record_handle(block_record_handle) # grant valid linking layout_handle = create_layout_tags(dwg, block_record_handle, owner=layout_dict.dxf.handle, tag_string=tag_string) logger.debug('creating entry in ACAD_LAYOUT dictionary for {}'.format(layout_name)) layout_dict[layout_name] = layout_handle # insert layout into the layout management table block_record.dxf.layout = layout_handle # link model space block record to layout # rename block to standard format (*Model_Space or *Paper_Space) if real_block_name != block_name: logger.debug('renaming BLOCK form {} to {}'.format(block_name, real_block_name)) dwg.blocks.rename_block(real_block_name, block_name) def create_layout_tags(dwg: 'Drawing', block_record_handle: str, owner: str, tag_string: str): # Problem: ezdxf was not designed to handle the absence of model/paper space LAYOUT entities # Layout structure is not initialized at this runtime phase logger.debug('creating LAYOUT entity for BLOCK_RECORD(#{})'.format(block_record_handle)) object_section = dwg.objects entitydb = dwg.entitydb tags = ExtendedTags.from_text(tag_string) layout_handle = entitydb.get_unique_handle() # create new unique handle tags.replace_handle(layout_handle) # set entity handle entitydb.add_tags(tags) # add layout entity to entity database object_section.add_handle(layout_handle) # add layout entity to objects section tags.noclass.set_first(DXFTag(330, owner)) # set owner tag acdblayout = tags.get_subclass('AcDbLayout') acdblayout.set_first(DXFTag(330, block_record_handle)) # link to block record return layout_handle def upgrade_to_ac1015(dwg: 'Drawing'): """ Upgrade DXF versions AC1012 and AC1014 to AC1015. """ def upgrade_layout_table(): if 'ACAD_LAYOUT' in dwg.rootdict: setup_model_space(dwg) # setup layout entity and link to proper block and block_record entities setup_paper_space(dwg) # setup layout entity and link to proper block and block_record entities else: raise DXFInternalEzdxfError("Table ACAD_LAYOUT should already exist in root dict.") def upgrade_layer_table(): logger.debug('upgrading LAYERS table') try: plot_style_name_handle = dwg.rootdict.get('ACAD_PLOTSTYLENAME') # DXFDictionaryWithDefault except DXFKeyError: raise DXFInternalEzdxfError("Table ACAD_PLOTSTYLENAME should already exist in root dict.") set_plot_style_name_in_layers(plot_style_name_handle) try: # do not plot DEFPOINTS layer or AutoCAD is yelling defpoints_layer = dwg.layers.get('DEFPOINTS') except DXFValueError: pass else: defpoints_layer.dxf.plot = 0 def set_plot_style_name_in_layers(plot_style_name_handle): logger.debug('setting layers "plot_style_name" attribute') for layer in dwg.layers: layer.dxf.plot_style_name = plot_style_name_handle def upgrade_dim_style_table(): logger.debug('upgrading DIMSTYLES table') dim_styles = dwg.dimstyles header = dim_styles._table_header dim_style_table = Tags([ DXFTag(100, 'AcDbDimStyleTable'), DXFTag(71, len(dim_styles)) ]) for entry in dim_styles: dim_style_table.append(DXFTag(340, entry.dxf.handle)) header.subclasses.append(dim_style_table) def upgrade_objects(): logger.debug('upgrading ACDBPLACEHOLDER entities in the OBJECTS section') upgrade_acdbplaceholder(dwg.objects.query('ACDBPLACEHOLDER')) def upgrade_acdbplaceholder(entities): for entity in entities: entity.tags.subclasses = entity.tags.subclasses[0:1] # remove subclass AcDbPlaceHolder # calling order is important! logger.info('Upgrading drawing to DXF R2000.') upgrade_layout_table() upgrade_layer_table() upgrade_dim_style_table() upgrade_objects() logger.debug('Setting DXF version to AC1015.') dwg.dxfversion = 'AC1015' dwg.header['$ACADVER'] = 'AC1015' def upgrade_to_ac1009(dwg: 'Drawing'): """ Upgrade DXF versions prior to AC1009 (R12) to AC1009. """ logger.info('Upgrading drawing to DXF R12.') logger.debug('Setting DXF version to AC1009.') dwg.dxfversion = 'AC1009' dwg.header['$ACADVER'] = 'AC1009' # as far I know, nothing else to do def cleanup_r12(dwg: 'Drawing'): """ Remove unsupported sections and tables, repair tag structure. Args: dwg: Drawing() object """ logger.info('Cleanup DXF R12 drawing.') if dwg.dxfversion > 'AC1009': return for section_name in ('CLASSES', 'OBJECTS', 'THUMBNAILIMAGE', 'ACDSDATA'): # unsupported sections for DXF R12 if section_name in dwg.sections: logger.debug('Deleting {} section.'.format(section_name)) dwg.sections.delete_section(section_name) if 'BLOCK_RECORDS' in dwg.sections.tables: logger.debug('Deleting BLOCK_RECORDS table.') del dwg.sections.tables['BLOCK_RECORDS'] def filter_subclass_marker(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]: """ Filter subclass marker from malformed DXF R12 files. (like from Leica Disto Units) Subclass markers in R12 files, creates subclasses in ExtendedTags(), which does not work with the DXF R12 attribute definitions. Other unsupported tags are not problematic, they are just ignored. Args: tagger: low level tagger """ found = 0 for tag in tagger: if tag.code == SUBCLASS_MARKER and tag.value.startswith('AcDb'): found += 1 else: yield tag if found: logger.debug('Filtered {} SUBCLASS marker from DXF R12 tag stream.'.format(found)) def tag_reorder_layer(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]: """ Reorder coordinates of legacy DXF Entities, for now only LINE. Args: tagger: low level tagger """ logger.debug('Reordering coordinate tags for LINE entity.') collector = None # type: Optional[List] for tag in tagger: if tag.code == 0: if collector is not None: # stop collecting if inside of an supported entity entity = collector[0].value yield from COORDINATE_FIXING_TOOLBOX[entity](collector) collector = None if tag.value in COORDINATE_FIXING_TOOLBOX: collector = [tag] tag = None # do not yield collected tag yet else: # tag.code != 0 if collector is not None: collector.append(tag) tag = None # do not yield collected tag yet if tag is not None: yield tag def fix_coordinate_order(tags, codes=(10, 11)): def extend_codes(): for code in codes: yield code # x tag yield code + 10 # y tag yield code + 20 # z tag def get_coords(code): # if x or y coordinate is missing, it is a DXFStructureError # but here is not the location to validate the DXF structure try: yield coordinates[code] except KeyError: pass try: yield coordinates[code + 10] except KeyError: pass try: yield coordinates[code + 20] except KeyError: pass coordinate_codes = frozenset(extend_codes()) coordinates = {} remaining_tags = [] insert_pos = None for tag in tags: # separate tags if tag.code in coordinate_codes: coordinates[tag.code] = tag if insert_pos is None: insert_pos = tags.index(tag) else: remaining_tags.append(tag) if len(coordinates) == 0: # no coordinates found, this is probably a DXFStructureError, # but here is not the location to validate the DXF structure, # just do nothing. return tags ordered_coords = [] for code in codes: ordered_coords.extend(get_coords(code)) remaining_tags[insert_pos:insert_pos] = ordered_coords return remaining_tags COORDINATE_FIXING_TOOLBOX = { 'LINE': partial(fix_coordinate_order, codes=(10, 11)), } def fix_classes(dwg): def remove_group_code_91(): logger.debug('Deleting group code 91 tags from CLASS entities for DXF Versions prior AC1018.') for cls in dwg.sections.classes: xtags = cls.tags xtags.noclass.remove_tags((91,)) if dwg.dxfversion <= 'AC1009': # DXF R12 and prior has no CLASSES return if dwg.dxfversion < 'AC1018': # remove group code 91, which is not supported prior to AC1018 remove_group_code_91() _MODEL_SPACE_LAYOUT_TPL = """ 0 LAYOUT 5 0 330 0 100 AcDbPlotSettings 1 2 DWFx ePlot (XPS Compatible).pc3 4 ANSI_A_(8.50_x_11.00_Inches) 6 40 5.8 41 17.8 42 5.8 43 17.8 44 215.9 45 279.4 46 0.0 47 0.0 48 0.0 49 0.0 140 0.0 141 0.0 142 1.0 143 14.53 70 11952 72 0 73 1 74 0 7 75 0 147 0.069 148 114.98 149 300.29 100 AcDbLayout 1 Model 70 1 71 0 10 0.0 20 0.0 11 12.0 21 9.0 12 0.0 22 0.0 32 0.0 14 0.0 24 0.0 34 0.0 15 0.0 25 0.0 35 0.0 146 0.0 13 0.0 23 0.0 33 0.0 16 1.0 26 0.0 36 0.0 17 0.0 27 1.0 37 0.0 76 0 330 0 """ _PAPER_SPACE_LAYOUT_TPL = """ 0 LAYOUT 5 DEAD 330 DEAD 100 AcDbPlotSettings 1 2 DWFx ePlot (XPS Compatible).pc3 4 ANSI_A_(8.50_x_11.00_Inches) 6 40 5.8 41 17.8 42 5.8 43 17.8 44 215.9 45 279.4 46 0.0 47 0.0 48 0.0 49 0.0 140 0.0 141 0.0 142 1.0 143 1.0 70 688 72 0 73 1 74 5 7 acad.ctb 75 16 147 1.0 148 0.0 149 0.0 100 AcDbLayout 1 Layout1 70 1 71 1 10 -0.7 20 -0.23 11 10.3 21 8.27 12 0.0 22 0.0 32 0.0 14 0.63 24 0.8 34 0.0 15 9.0 25 7.2 35 0.0 146 0.0 13 0.0 23 0.0 33 0.0 16 1.0 26 0.0 36 0.0 17 0.0 27 1.0 37 0.0 76 0 330 DEAD """