caddxfezdxf

Rasterising only selected area of a CAD DXF file


Given a DXF file (2D CAD drawing), is it somehow possible to rasterise only part of it? Preferably in Python's ezdxf. By the part of it, I mean the selected rectangular area, not a single layer.

Background: I'm struggling to rasterise quite a big DXF file with decent DPI in a reasonable time, so I thought that maybe there's a way to speed up the process by parallelising rasterising different parts of the drawing. I'm using ezdxf with matplotlib backend.


Solution

  • This solution renders the DXF file in 4 tiles including filtering the DXF entities outside the rendering area. But the calculation of the bounding boxes is also costly and the entities in the overlapping area are rendered multiple times, this means this solution takes longer as a single-pass rendering. But it shows the concept. The images fit perfect together the space is left to show that this are 4 images:

    The 4 tiles side by side

    import matplotlib.pyplot as plt
    import random
    
    import ezdxf
    from ezdxf.addons.drawing import RenderContext, Frontend
    from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
    from ezdxf import bbox
    from ezdxf.math import BoundingBox2d
    
    COLORS = list(range(1, 7))
    DPI = 300
    WIDTH = 400
    HEIGHT = 200
    LEFT = 0
    BOTTOM = 0
    
    doc = ezdxf.new()
    msp = doc.modelspace()
    
    
    def random_points(count):
        for _ in range(count):
            yield WIDTH * random.random(), HEIGHT * random.random()
    
    
    for s, e in zip(random_points(100), random_points(100)):
        msp.add_line(s, e, dxfattribs={"color": random.choice(COLORS)})
    
    # detecting the drawing extents by ezdxf can take along time for big files!
    cache = bbox.Cache()  # reuse bounding boxes for entity filtering
    rect = bbox.extents(msp, cache=cache)
    WIDTH = rect.size.x
    HEIGHT = rect.size.y
    LEFT = rect.extmin.x
    BOTTOM = rect.extmin.y
    
    
    VIEWPORT_X = [LEFT, LEFT + WIDTH / 2, LEFT, LEFT + WIDTH / 2]
    VIEWPORT_Y = [BOTTOM, BOTTOM, BOTTOM + HEIGHT / 2, BOTTOM + HEIGHT / 2]
    
    ctx = RenderContext(doc)
    for quarter in [0, 1, 2, 3]:
        # setup drawing add-on:
        fig = plt.figure(dpi=300)
        ax = fig.add_axes([0, 0, 1, 1])
        out = MatplotlibBackend(ax)
    
        # calculate and set render borders:
        left = VIEWPORT_X[quarter]
        bottom = VIEWPORT_Y[quarter]
        ax.set_xlim(left, left + WIDTH / 2)
        ax.set_ylim(bottom, bottom + HEIGHT / 2)
    
        # set entities outside of the rendering area invisible:
        # Bounding box calculation can be very costly, especially for deep nested
        # block references! If you did the extents calculation and reuse the cache
        # you already have paid the price:
        render_area = BoundingBox2d(
            [(left, bottom), (left + WIDTH / 2, bottom + HEIGHT / 2)])
    
        for entity in msp:
            entity_bbox = bbox.extents([entity], cache=cache)
            if render_area.intersect(entity_bbox):
                entity.dxf.invisible = 0
            else:
                entity.dxf.invisible = 1
    
        # finalizing invokes auto-scaling!
        Frontend(ctx, out).draw_layout(msp, finalize=False)
    
        # set output size in inches
        # width = 6 in x 300 dpi = 1800 px
        # height = 3 in x 300 dpi = 900 px
        fig.set_size_inches(6, 3, forward=True)
    
        filename = f"lines{quarter}.png"
        print(f'saving to "{filename}"')
        fig.savefig(filename, dpi=300)
        plt.close(fig)
    
    

    The draw_layout() method has an argument filter_func to specify a function which accepts a DXF entity as argument and returns True or False to render or ignore this entity. This would be an alternative to filter the entities outside of the rendering area without altering the DXF content.

    UPDATE: a refined example can be found at github