Skip to main content
OctopusPDF Guide

Interactive Charts to Markdown: Extracting SVG and Vector Data from PDFs

Extract SVG from PDF Charts: Vector Data Extraction and the PDF to Markdown Track Short answer: you cannot extract SVG from a PDF with a Markdown converter — no…

Extract SVG from PDF Charts: Vector Data Extraction and the PDF to Markdown Track

Short answer: you cannot extract SVG from a PDF with a Markdown converter — not a good one, not a bad one. Markdown is plain text and has no primitive for vector paths, data series, or interactivity. Getting the chart out of the PDF is a graphics job (pdftocairo, mutool, Inkscape, or PyMuPDF); getting the prose out of the PDF is a text job. Teams that need both run two tracks, and the teams that don't plan for the second track end up with a Markdown file full of axis labels and no data.

So the phrase "extract SVG from PDF" and the phrase "PDF to Markdown" describe two different operations on the same file. This article covers the vector data extraction track first — step by step, with commands and a runnable calibration script — then compares the two ways to produce the Markdown half, including how to verify the token-savings claims vendors publish.

Search intent: people searching "extract SVG from PDF" want the chart itself — its geometry, its series, its editable paths — out of a PDF, not a picture of the chart and not the caption underneath it. They are usually surprised that their Markdown pipeline is not the tool for it. This article explains why, and what to use instead.

At a Glance

  • Markdown cannot hold a chart. Labels, legends, axis numbers, and captions that live in the PDF's text layer can survive conversion. The drawn bars, lines, arcs, and axis geometry cannot, and neither can interactivity.
  • Extracting SVG from a PDF needs a vector toolchain, not a converter. pdftocairo -svg (Poppler), mutool draw -F svg (MuPDF), Inkscape's command-line exporter, and PyMuPDF's get_drawings() / get_svg_image() all do this. Pick by whether you want a file or a data structure.
  • Check vector vs raster first. A PDF chart can be live vector geometry or a flattened image. If it is an image, no SVG extractor will help and you need OCR plus manual digitizing.
  • Coordinates in the SVG are not values. You must apply the SVG's own transform matrices and then calibrate the axis against two known ticks before any bar height becomes a number.
  • The real pattern is two-track: Markdown for text, headings, tables, and captions; a separate graphics pass for vector content; then link or embed the SVG back into the Markdown by reference.

How to Extract SVG from PDF Charts

Step 1 — Decide whether the chart is vector or raster

This determines everything that follows. Three checks, in ascending order of effort:

Eyeball test. Open the PDF and zoom to 800%. A vector chart stays crisp; a raster chart turns into visible pixels. Try to select a data label with the text cursor — if the axis numbers highlight, the chart has a text layer, which is a strong signal that the chart itself is vector.

Command-line inventory. Poppler's pdfimages lists every embedded raster image with its pixel dimensions:

pdfimages -list report.pdf
# page   num  type   width height color comp  bpc  object ID
#    2     0  image   1240   860   rgb     3    8  [ 12 0 R]

A page-sized image covering the chart region on page 2 means the chart was flattened. If the inventory comes back empty for that page, the chart is drawn with vector operators.

Programmatic decision rule. PyMuPDF gives you both counts in one line:

import fitz  # PyMuPDF
doc = fitz.open("report.pdf")
page = doc[1]                      # page 2, zero-indexed
print("vector paths:", len(page.get_drawings()))
print("raster images:", len(page.get_images(full=True)))
  • Hundreds of vector paths, zero or one unimportant image → vector. Go to Step 2.
  • One page-width image, few or no paths, no extractable text → raster. Skip to the OCR fallback below.

Step 2 — Export the vector content

What you want Tool Command
One page as an SVG file Poppler pdftocairo pdftocairo -svg -f 3 -l 3 report.pdf chart-p3.svg
One page as SVG, alternative backend MuPDF mutool mutool draw -F svg -o chart-p%d.svg report.pdf
Page opened for cropping a single object Inkscape 1.x CLI inkscape --pdf-page=3 --export-type=svg --export-filename=chart.svg report.pdf
Whole page as an SVG string in memory PyMuPDF page.get_svg_image(text_as_path=False)
Vector primitives as Python objects, no file PyMuPDF or pdfplumber page.get_drawings(); or page.curves / page.lines / page.rects

Three things worth knowing before you commit to one of these:

pdftocairo outputs one page at a time. The -f / -l flags set first and last page. Without them you get page 1 only, which is the single most common reason people think the tool is broken.

MuPDF's SVG backend may emit text as <path> outlines rather than <text> elements. If your goal is the label layer, open the output and check for <text> tags. If they are missing, the labels are now unsearchable outlines — usable visually, useless as data.

text_as_path=False is the flag that keeps labels as text in PyMuPDF's SVG output. The default is True, which is exactly the wrong setting for this job.

Terminal running pdftocairo -svg, then the exported SVG zoomed to 800 percent

Figure 1: A page exported with pdftocairo -svg and opened at 800% zoom. The bar edges stay sharp because the geometry is now vector elements in an SVG file — not because the PDF was converted to Markdown.

Step 3 — Turn SVG geometry into chart data

An SVG file is not a dataset. It is a list of coordinates in page space, and there are two coordinate spaces in play, which is where most first attempts fail.

Read the transform before you read the numbers. A pdftocairo export typically wraps its content like this:

<g transform="matrix(1,0,0,-1,0,792)">
  <path d="M 120 612 L 168 612 L 168 372 L 120 372 Z" fill="#1f77b4"/>
</g>

PDF user space has its origin at the bottom-left with y increasing upward. SVG display space has its origin at the top-left with y increasing downward. The matrix matrix(1,0,0,-1,0,792) is the bridge: it negates y (d = -1) and translates by the page height in points (f = 792 for US Letter). A path vertex at PDF y = 420 becomes SVG y = 792 − 420 = 372.

Skip that step and you calibrate in the wrong frame: the baseline ends up above the top tick, bar heights come out negative or inverted, and a value-80 bar reads as zero. Apply every transform on every ancestor element, in order, before you touch a single coordinate.

Then calibrate against two ticks. In the display frame above, suppose the y-axis baseline (value 0) sits at y = 612 and the top tick (value 80) sits at y = 132. Because y now grows downward, the slope is negative:

  • span = 612 − 132 = 480 px for 80 units → 6 px = 1 unit
  • value(y) = (612 − y) / 6
  • a bar whose top edge sits at y = 372 therefore has a value of (612 − 372) / 6 = 40

That arithmetic is the whole method. Two reference ticks give you the linear map; every <rect> height, <path> vertex, and <circle> center then converts to a data value. Read the ticks from the SVG's own <text> elements rather than hard-coding them wherever the export keeps labels as text.

Do not expect the SVG to carry series names. SVG has no concept of a data series. Color and draw order are the only things tying a <path> to "Product A" versus "Product B", and you supply that mapping from the legend.

End-to-end script: SVG → calibrated CSV

Minimal but runnable. It applies nested transforms, reads axis ticks out of the SVG, fits the linear calibration, and writes one row per rectangle.

#!/usr/bin/env python3
"""
svg_vector_to_csv.py

    pdftocairo -svg -f 3 -l 3 report.pdf chart-p3.svg
    python svg_vector_to_csv.py chart-p3.svg out.csv

Requires: pip install lxml
Tested against Poppler-style exports; check the tick detection on your own files.
"""
import csv
import re
import sys

from lxml import etree

SVG = "http://www.w3.org/2000/svg"


def parse_matrix(t):
    """Return (a,b,c,d,e,f) for a transform string containing matrix(...)."""
    if not t:
        return None
    m = re.search(
        r"matrix\(\s*([-\d.eE]+)[,\s]+([-\d.eE]+)[,\s]+([-\d.eE]+)[,\s]+"
        r"([-\d.eE]+)[,\s]+([-\d.eE]+)[,\s]+([-\d.eE]+)\s*\)",
        t,
    )
    return tuple(float(v) for v in m.groups()) if m else None


def compose(m1, m2):
    """Parent matrix m1 applied after child matrix m2."""
    a1, b1, c1, d1, e1, f1 = m1
    a2, b2, c2, d2, e2, f2 = m2
    return (
        a1 * a2 + c1 * b2,
        b1 * a2 + d1 * b2,
        a1 * c2 + c1 * d2,
        b1 * c2 + d1 * d2,
        a1 * e2 + c1 * f2 + e1,
        b1 * e2 + d1 * f2 + f1,
    )


def apply(m, x, y):
    a, b, c, d, e, f = m
    return a * x + c * y + e, b * x + d * y + f


def walk(el, matrix=(1, 0, 0, 1, 0, 0), rects=None, ticks=None):
    """Depth-first walk accumulating transforms; collect rects and numeric text."""
    if rects is None:
        rects, ticks = [], []
    own = parse_matrix(el.get("transform"))
    if own:
        matrix = compose(matrix, own)

    tag = etree.QName(el).localname
    if tag == "rect":
        x, y = float(el.get("x", 0)), float(el.get("y", 0))
        w, h = float(el.get("width", 0)), float(el.get("height", 0))
        pts = [apply(matrix, x, y), apply(matrix, x + w, y),
               apply(matrix, x, y + h), apply(matrix, x + w, y + h)]
        xs, ys = [p[0] for p in pts], [p[1] for p in pts]
        rects.append({
            "x_center": (min(xs) + max(xs)) / 2,
            "top_y": min(ys),            # y grows downward: min y is the top edge
            "width": max(xs) - min(xs),
            "fill": el.get("fill", ""),
        })
    elif tag == "text":
        try:
            value = float("".join(el.itertext()).strip().replace(",", ""))
        except ValueError:
            value = None
        if value is not None:
            x, y = float(el.get("x", 0)), float(el.get("y", 0))
            ticks.append((value, apply(matrix, x, y)[1]))

    for child in el:
        walk(child, matrix, rects, ticks)
    return rects, ticks


def calibrate(ticks):
    """Least-squares fit of value = k*y + b over the axis tick labels."""
    n = len(ticks)
    if n < 2 or len({round(y, 3) for _, y in ticks}) < 2:
        raise SystemExit("need at least two distinct axis tick labels")
    sx = sum(y for _, y in ticks)
    sy = sum(v for v, _ in ticks)
    sxx = sum(y * y for _, y in ticks)
    sxy = sum(v * y for v, y in ticks)
    k = (n * sxy - sx * sy) / (n * sxx - sx * sx)
    return k, (sy - k * sx) / n


def main(svg_path, csv_path):
    root = etree.parse(svg_path).getroot()
    rects, ticks = walk(root)
    k, b = calibrate(ticks)
    with open(csv_path, "w", newline="") as fh:
        writer = csv.writer(fh)
        writer.writerow(["x_center", "top_y", "value", "width", "fill"])
        for r in rects:
            writer.writerow([
                round(r["x_center"], 2),
                round(r["top_y"], 2),
                round(k * r["top_y"] + b, 3),
                round(r["width"], 2),
                r["fill"],
            ])


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Two caveats. Series names are still missing — add a legend-to-fill mapping by hand or from the PDF text layer. And the tick detector only fires when labels survive as <text>; if your export outlined the text, hard-code the two reference ticks you read off the figure.

OCR fallback for raster charts

When the inventory in Step 1 proves the chart is a flattened image, there is no vector geometry to export. Render the page at high DPI and OCR it:

pdftoppm -r 300 -png -f 3 -l 3 report.pdf page
tesseract page-3.png chart-ocr --psm 6

OCR recovers labels, legends, and axis numbers. It does not recover bar heights or line values — those were never text. To digitize a raster chart's data you need a manual or semi-automatic tool such as WebPlotDigitizer, or the original source data from whoever published the chart. Budget for this explicitly: it is the most expensive path in the entire pipeline and it is invisible in any "convert your PDFs to Markdown" comparison.

Worked Example: The Label Layer Is Not the Data Layer

Illustrative data. Every number, coordinate, and path below is fabricated to demonstrate the axis-calibration arithmetic and the transform flip. It does not come from a real report. Run the script above against your own exported SVG to produce real output.

The shape of the output when you run both tracks against the same page — a grouped bar chart, 4 categories, 2 series, y-axis 0–80 in thousands of units:

Track 1 — Markdown conversion output (excerpt):

## Units Sold by Quarter

Q1 Q2 Q3 Q4
0 20 40 60 80
Product A Product B
Source: quarterly report, p. 2

Track 2 — pdftocairo -svg output (excerpt, coordinates in display space after the flip):

<g transform="matrix(1,0,0,-1,0,792)">
  <path d="M 120 612 L 168 612 L 168 372 L 120 372 Z" fill="#1f77b4"/>
  <path d="M 180 612 L 228 612 L 228 288 L 180 288 Z" fill="#ff7f0e"/>
  ...
</g>

Calibrated with 6 px per unit: the first bar's top edge at y = 372 → 40 units; the second at y = 288 → 54 units.

What you need In the Markdown? In the SVG?
Chart title and source caption Yes No
Category names Yes, as loose text No
Axis tick values Yes, but stripped of position Yes, with coordinates
Series legend labels Yes, unordered No
Bar heights / data values No Yes, after axis calibration
Series-to-path mapping No No — you supply it
Interactivity No No — SVG is static

The Markdown track gave you the caption. The SVG track gave you the geometry. Neither gave you a chart, and only together do they give you a document a reader can actually use.

Edge Cases That Break Real Extractions

Each of these is a frequent cause of a pipeline that works on a demo PDF and fails on a production corpus.

  • CMYK and spot colours. SVG has no spot-colour model, so Pantone inks and CMYK fills are approximated at export; colour no longer identifies a series reliably, and the fill value in your CSV may not match the legend swatch. Match on draw order, and read the PDF's separation space separately if brand-accurate colour matters.
  • Clipping paths and masks. Bars are often drawn full-size and clipped to the plot area. The d attribute then reports the unclipped rectangle, not the visible top edge, and your heights will be wrong until you intersect the geometry with the clipPath rectangle.
  • Transparency and gradients. A gradient fill means the visible top of a bar may not correspond to any single path vertex, and alpha-composited overlaps can merge two series into one apparent shape; flatten or threshold the alpha channel before extracting.
  • Multi-page and spanning charts. A chart that continues across a spread, or a legend repeated on every page, produces duplicate data points; de-duplicate on (series, category) after calibration rather than assuming a one-page-to-one-chart mapping.
  • Multi-axis and logarithmic scales. Two y-axes on one plot require two independent calibrations, assigned by which axis the geometry is anchored to; a log axis needs value = 10**(k*y + b), fit the same way but in log space, or the residuals will look fine and the values will be wrong.
  • Scanned charts and OCR accuracy. OCR error rates on small axis labels run well above the noise floor of the data you are trying to recover, so record a confidence threshold, keep the raw OCR text alongside the parsed number, and spot-check a sample of pages manually before trusting an automated batch.
Rasterized chart at 800 percent beside the same chart exported as crisp SVG

Figure 2: The Step 1 test, made visual. Left, a rasterized chart from a flattened PDF page — pixelated at 800%. Right, the same page exported to SVG. Only the right-hand case can be extracted, edited, or reduced to data values.

The Markdown Track: Tool Comparison

With the graphics pass settled, the Markdown half reduces to one procurement question: does the file have to leave the device? Three names come up repeatedly in that discussion — one browser-local converter and two hosted services.

The figures below are vendor-published and dated: as of early 2025, and this is exactly the class of detail that changes. Confirm current limits and pricing on each vendor's own site before planning capacity around them.

Dimension Browser-local converter (e.g. OctopusPDF) Hosted services (e.g. BlazeDocs, PDF Mavericks)
Where the file is processed User's device, in the browser Vendor servers, after upload
Output Markdown with headings, lists, tables preserved "where possible" Markdown; structural fidelity varies by vendor
Published free tier 3 conversions/day, files up to 20 MB Not published for the two services named here
Published paid tier Unlimited conversions, 100 MB files, batch mode BlazeDocs listed at $9.99/month; PDF Mavericks pricing not published
Confidential documents Source file never leaves the device Requires sending the file to a third party
Setup Open a page, drop in a file Create an account, upload, download
Chart / SVG extraction Not provided — separate graphics pass required Not provided — separate graphics pass required

Three things actually differ, and the rest is noise:

  1. Privacy. A browser-local tool never receives your file; a hosted service by definition does. For legal, medical, or proprietary research PDFs entering an AI pipeline, this is a structural difference, not a feature comparison.
  2. Published limits. One of these three publishes file-size and volume limits; the other two do not, as of the date above. That is a planning risk either way — the published numbers can change, and the unpublished ones can only be confirmed by asking.
  3. Operational model. A hosted service gives a team one account, one pipeline, and vendor-owned maintenance. A browser-local tool gives each person a tab and no procurement step.

Everything else — headings preserved, tables preserved, token reduction — comes from choosing Markdown as the output format at all, not from choosing a particular vendor.

About the 30–50% token-savings figure. It is published by a conversion vendor in its own documentation, which makes it a vendor claim rather than an independent benchmark, and it applies to text-and-table-heavy documents rather than chart-heavy ones. Verify it on your own corpus before you build a capacity plan on it — the check takes about two minutes:

import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
raw = enc.encode(open("report.txt").read())     # pdftotext output
md  = enc.encode(open("report.md").read())      # converter output
print(len(raw), len(md), round(100 * (1 - len(md) / len(raw))))

Run that on ten of your own documents and you will have a defensible number. Note that chart-heavy pages behave differently: a full-page chart may convert to a title, a legend, and eight axis numbers — cheap in tokens, and near-worthless as retrieval context.

When to Choose Which

Choose a browser-local converter when: your PDFs contain confidential legal, medical, or proprietary research content; you convert a handful of documents a day; you are testing whether a conversion approach is worth building a pipeline around; or you want published, plannable limits rather than a sales conversation.

Choose a hosted service when: your team needs one centralized, vendor-maintained pipeline rather than per-person browser sessions; your volume or file sizes exceed what a local tool documents; you want an account your organization can procure and administer formally; and your documents are not confidential.

Do not choose either one for the charts. Both leave the SVG problem exactly where it was.

FAQ

How do I extract SVG from a PDF chart?

You extract SVG from a PDF chart with a vector-capable PDF renderer, not with a Markdown converter. Use pdftocairo -svg -f 3 -l 3 report.pdf chart-p3.svg for a single page as a file, mutool draw -F svg as an alternative backend, Inkscape's CLI when you want to crop one object interactively, or PyMuPDF's page.get_drawings() when you want vector primitives as Python data rather than as a file. All four require the chart to be live vector geometry; none of them work on a flattened raster image.

Can Markdown store an interactive chart from a PDF?

No. Markdown is plain text and has no primitive for vector paths, data series, or interactivity. A Markdown file can reference an image or link to an external SVG, but the chart's geometry has to be extracted separately with a vector toolchain before Markdown can point at it.

Does converting a PDF to Markdown lose the data inside its charts?

Converting a PDF to Markdown keeps the chart's text labels and loses the chart's drawn geometry. Title, caption, legend entries, and axis numbers that live in the PDF's text layer can survive conversion as text; bar heights, line values, and axis geometry cannot, because Markdown has nowhere to put them. Treat Markdown output as the label layer and the SVG export as the data layer — they are not substitutes.

Why do my extracted values come out negative or inverted?

Because the SVG's group transform was not applied. PDF user space has y increasing upward from the bottom-left; SVG display space has y increasing downward from the top-left. A transform such as matrix(1,0,0,-1,0,792) negates y and translates by the page height, and any coordinate read from inside that group without applying it is in the wrong frame. See the script in Step 3 for a transform-aware walk.

How much does PDF-to-Markdown conversion actually save on tokens?

Vendors report substantial reductions for text-heavy documents while preserving tables, headings, and lists, but the widely quoted figure originates with a conversion vendor rather than an independent benchmark. Measure it on your own files with tiktoken before you plan capacity around it. On chart-heavy pages the token count drops much further and the retrievable information drops with it.

Is a hosted converter safe for confidential PDFs?

A hosted converter is not safe for confidential PDFs unless your organization has explicitly approved sending those files to a third party. The hosted services named in the comparison table both require the file to leave your device and be processed on vendor infrastructure; a browser-local converter processes the document without uploading it. For legal, medical, or proprietary research documents, that distinction decides the choice before any other criterion is considered.

Why does my converted Markdown show axis numbers but no chart?

Because the axis numbers are text in the PDF's text layer and the chart is drawn geometry. Markdown can carry the first and has no syntax for the second. The fix is to export the page to SVG and embed or link that SVG from the Markdown file, rather than expecting the converter to reconstruct the chart.

Verdict

Markdown conversion and SVG extraction are two problems, and the article title only makes sense once you separate them. No converter on the market reconstructs a chart as vector data in Markdown, because the format has no way to hold it. Plan a two-track pipeline from the start: convert the document to Markdown for text, headings, tables, and captions, and extract SVG from the PDF separately with pdftocairo, mutool, Inkscape, or PyMuPDF — then embed or link the SVG from the Markdown file.

On the Markdown track, the deciding question is whether the file may leave the device, followed by whether you need a plannable published quota or a centrally administered account. Both categories leave the charts untouched, and both are commodity choices next to the graphics work. Pick one, timestamp your assumptions, and revisit them when the vendors change their limits.

Regardless of which Markdown converter you pick, the graphics pass is non-negotiable, and it is where the actual work lives.

Key Facts

  • Two coordinate frames, one bridge. PDF user space is y-up from the bottom-left; SVG display space is y-down from the top-left. A group transform such as matrix(1,0,0,-1,0,792) negates y and translates by the page height in points. Apply every ancestor transform before calibrating, or values come out inverted.
  • Calibration needs exactly two known ticks. value = k·y + b, where k is negative for a y-down frame; a log axis needs the same fit in log space, and a dual-axis chart needs two independent fits assigned by anchor geometry.
  • SVG cannot carry series identity. No series names, no data values, no animation semantics beyond SMIL/CSS on static paths, and no interactivity model that a Markdown file can embed. Series-to-path mapping is always supplied by you, from the legend or the PDF text layer.
  • The common silent failure is clipping. Bars drawn full-height and clipped to the plot area report unclipped coordinates, so intersect geometry with the clipPath rectangle before reading a top edge.
  • Colour is a weak join key. CMYK and spot colours are approximated at SVG export, and gradient or alpha-composited fills may not correspond to any single path vertex.
  • Raster fallback is a different pipeline. Once Step 1 proves a chart is flattened, OCR recovers labels but never values; digitizing is manual or semi-automatic and should be scoped as its own workstream.