Extracting Vector Paths from PDF Floor Plans

A companion to PDF & Raster Floor Plan Extraction for the case that saves the most work: the PDF already contains the geometry, and nothing needs to be traced from pixels at all.

Classify Before You Render

Classifying a PDF page before deciding how to parse it One decision with four outcomes, evaluated before any rendering. A page containing path operators is vector and its geometry is extracted directly with no tracing. A page whose content is a single large image is raster and must be rendered and traced. A page with both is a hybrid, where the vectors are preferred and the raster preview ignored. A page with only text is not a plan at all — a room schedule or a cover sheet — and should be skipped. Four kinds of page, and only one of them needs tracing What is actually inside this PDF? checked before any pixel is rendered path operators Vector extract directly, no tracing one big image Raster render and trace both Hybrid prefer the vectors, ignore the preview text only Not a plan a schedule or a cover sheet

The hybrid case is the common surprise. Many “scans” are vector drawings with a raster preview drawn on top; extracting the paths skips the entire tracing pipeline and its lost recall.

What each PDF parsing path costs and recovers Grouped bars over three parsing paths. Vector extraction reaches 0.99 wall recall in 0.4 seconds per page. Rendering and tracing at 300 dpi reaches 0.93 recall in 8 seconds per page. A photographed plan reaches only 0.71 recall and takes 11.2 seconds because of the extra perspective correction. The vector path wins on both axes, when it exists 0 5 10 1 2 3 1 vector extraction 2 raster @300dpi 3 photographed recall (0-1) / seconds per page wall recall seconds per page

Twenty times faster and six points more recall. Checking for a vector layer is the single highest-return line of code in a raster pipeline.

The measurement is stark: vector extraction is roughly twenty times faster than rendering and tracing, and recovers six more points of wall recall — because it is reading the geometry rather than inferring it.

So the first thing a PDF pipeline should do is look at what the page actually contains. A page with m/l/re operators has real geometry. A page whose entire content is one Do referencing a large image is a scan. A page with both is a hybrid, and preferring the vectors is almost always right: the raster is a preview for viewers that cannot render the paths.

Walking the Content Stream

The PDF content-stream operators a floor-plan extractor has to handle A table of seven PDF content-stream operators. The moveto and lineto operators produce the wall runs being extracted. Cubic Bézier operators must be flattened, at about a five millimetre tolerance. The rectangle operator expands to exactly four segments. The concatenate-matrix operator must be composed or coordinates come out wrong. Save and restore state require maintaining a matrix stack. Draw XObject requires recursing into the referenced object with the current matrix. A clip path means geometry outside it is invisible on the page. Seven operators, and one of them is where the bugs live Content-stream operator Operator Means Handling m / l moveto / lineto ● the wall runs you want c / v / y cubic Bézier flatten at 5 mm tolerance re rectangle ● four segments, exactly cm concatenate matrix △ compose or coordinates are wrong q / Q save / restore state △ maintain a matrix stack Do draw XObject recurse with the current matrix W n clip path geometry outside it is invisible The matrix stack is the whole difficulty: everything else is bookkeeping.

Coordinates are meaningless without the matrix stack. A wall extracted from inside an XObject, without composing the transforms that placed it, lands somewhere plausible and wrong.

Extraction is an interpreter for a small subset of the PDF content-stream language. The operators that produce geometry are few; the difficulty is entirely in the current transformation matrix.

PDF coordinates are expressed in whatever space the enclosing transforms establish. A cm operator concatenates a matrix; q and Q push and pop the graphics state, including that matrix; a Do operator draws a form XObject that has its own matrix, applied on top of the current one. A wall extracted from three levels of nesting without composing all four matrices comes out at plausible coordinates that are wrong by an arbitrary transform.

Minimal Working Example

import logging

import numpy as np
import pdfplumber
from shapely.geometry import LineString

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

PT_PER_INCH = 72.0


def classify_page(page) -> str:
    """Decide how a page should be parsed before spending anything on it."""
    n_paths = len(page.lines) + len(page.rects) + len(page.curves)
    n_images = len(page.images)
    if n_paths >= 50:
        return "hybrid" if n_images else "vector"
    if n_images:
        return "raster"
    return "not_a_plan"


def extract_segments(pdf_path: str, page_no: int, *, scale_denom: float = 100.0
                     ) -> list[LineString]:
    """Pull wall-like segments out of a vector PDF page, in metres."""
    try:
        with pdfplumber.open(pdf_path) as pdf:
            page = pdf.pages[page_no]
            kind = classify_page(page)
            if kind not in ("vector", "hybrid"):
                raise ValueError(f"page {page_no} is {kind}; use the raster pipeline")

            # pdfplumber has already composed the CTM for us: coordinates are in
            # PDF user space (points), origin bottom-left.
            segs: list[tuple[tuple[float, float], tuple[float, float]]] = []
            for ln in page.lines:
                segs.append(((ln["x0"], ln["y0"]), (ln["x1"], ln["y1"])))
            for r in page.rects:
                x0, y0, x1, y1 = r["x0"], r["y0"], r["x1"], r["y1"]
                segs += [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)),
                         ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))]
            for c in page.curves:
                pts = c.get("pts") or []
                segs += list(zip(pts, pts[1:]))          # already flattened by pdfplumber
    except (OSError, IndexError) as exc:
        logger.error("cannot read %s page %d: %s", pdf_path, page_no, exc)
        raise

    # points -> inches -> paper metres -> real metres at the drawing scale
    k = (1.0 / PT_PER_INCH) * 0.0254 * scale_denom
    out = [LineString([(a[0] * k, a[1] * k), (b[0] * k, b[1] * k)])
           for a, b in segs if a != b]
    logger.info("page %d: %d segment(s) at 1:%g", page_no, len(out), scale_denom)
    return out

The scale conversion is the part with no universal answer. A PDF’s user space is points on paper, so converting to real metres needs the drawing’s scale denominator — 1:100, 1:50 — which is written on the title block and not in the file. Reading it from the title block by pattern, or carrying it per building in the ingest record, is more reliable than inferring it from a known door width, though that inference is a useful cross-check.

Common Errors & Fixes

Geometry is upside down. PDF user space has its origin at the bottom-left with Y increasing upward, which happens to match a metric world frame — so if the output is inverted, something in the pipeline applied a raster-style Y-flip that was not needed here.

The building is the wrong size by a round factor. The scale denominator is wrong. A 1:50 drawing parsed as 1:100 comes out at half size, which is instantly recognisable once the total extent is checked against a plausible building — worth asserting explicitly, exactly as the cleanup stage asserts that geometry is in metres.

Only part of the plan appears. A clip path is limiting what is drawn, and the extractor is ignoring W n. Everything outside the clip is invisible on the page and should be discarded rather than extracted.

Curves come out as chords. Bézier flattening tolerance is too coarse. Five millimetres at drawing scale is a reasonable default; anything coarser turns a curved wall into a polygon a reader can see the facets on.

Integration Point

The output is a segment list in metres, which is exactly what the SVG/DWG parsing workflows branch produces — so it joins the same shared tail: geometry cleanup, then wall and door detection.

Where the page is a hybrid, the raster layer still has one use: OCR. Text in a vector PDF is usually extractable directly, but where the drawing was traced and the labels left as an image, the label OCR path runs on the raster while the geometry comes from the vectors.

Frequently Asked Questions

How do I tell a vector PDF from a scan without opening it visually?

Count path objects. A vector floor plan has hundreds to thousands of line, rectangle and curve objects per page; a scan has none and one large image. The classifier in the example uses a threshold of fifty path objects, which separates the two cleanly in practice — a scanned page with a vector title block might have a dozen, and a real drawing has far more. The check costs milliseconds and runs before any rendering, which is the point.

Which library should I use?

pdfplumber for extraction and pypdf for structure. pdfplumber composes the transformation matrices and flattens curves for you, which removes the part of the job most likely to produce subtly wrong coordinates, and exposes lines, rects and curves in page space directly. pypdf is better for reading the document structure — page count, XObject inventory, whether a text layer exists — and for the classification pass. Using both is normal; writing your own content-stream interpreter is not worth it unless you have a case neither handles.

What if the drawing scale is not written anywhere?

Infer it from a known dimension and verify against a second one. Door openings are the most reliable anchor because they cluster tightly around 0.9 metres in almost all building stock: extract the gap widths, find the modal value, and the ratio to 0.9 gives the scale. Verify with a second invariant — corridor widths cluster near 1.8 metres, storey heights near 3 to 4 — and reject rather than publish if the two disagree, since a wrong scale is a building that is silently the wrong size.

This page is a companion to PDF & Raster Floor Plan Extraction, part of the Automated Floor Plan Parsing & Vectorization section.