---
title: "PDF to Markdown: pull the text out, then rebuild the marks"
slug: pdf-to-markdown
excerpt: "Start from What pdftotext and markitdown gave in the test for the T1, T2 and T3 command lines and lines from their output."
quickAnswer: "The section What pdftotext and markitdown gave in the test quotes the T1, T2 and T3 command lines with lines from each output. Under Markdown marks that did not come back, read the markup counts line from the test log. Putting the marks back is set out as numbered steps in Rebuild the Markdown by hand."
keyTakeaways:
  - "See A PDF with no text layer for T4 and T5 on scan.pdf."
  - "The T6 error line and the last T7 stderr line sit under When the file name is wrong."
  - "Check Markdown marks that did not come back for the link, code and quote lines from notes.md."
  - "Version lines for both commands are quoted in What pdftotext and markitdown gave in the test."
  - "Follow the numbered list in Rebuild the Markdown by hand once your text file exists."
publishedDate: 2026-10-21
dateModified: 2026-10-21
author: Bobby Huang
cluster: B
role: post
status: published
faq:
  - q: "Why did the table come apart?"
    a: "Read the T1, T2 and T3 table lines quoted under What pdftotext and markitdown gave in the test."
  - q: "Can either command read a PDF that is only a picture?"
    a: "The section A PDF with no text layer covers T4 and T5 from this test."
  - q: "What if the command says it can't open the file?"
    a: "Go to When the file name is wrong for the T6 and T7 runs on nofile.pdf."
  - q: "Did the link address come back?"
    a: "Check the link line in Markdown marks that did not come back."
related:
  - markdown-document-conversion
  - word-to-markdown
  - markdown-to-pdf
---

So how do I get Markdown out of a PDF? Pull the text out with pdftotext or markitdown, then put the Markdown marks back yourself. In T1, T2 and T3 of the test log, neither command gave back a heading mark, a table pipe, a link target or a code fence.

To convert PDF to Markdown here, you run one command and then edit by hand. What follows rests on one test log, and that log came from one pass of the script on one computer. For the other routes between formats, start at [the hub on Markdown document conversion](markdown-document-conversion.md).

## What pdftotext and markitdown gave in the test

Everything started from notes.md, a Doe file of 32 lines that the test log prints in full. It holds front matter, four headings, two bullet lines, two numbered lines, a three-column table, one fenced Python block, one quote line, a web page link and a single image line. P0 in the test log made the PDF from it:

```bash
pandoc notes.md --pdf-engine=typst -o doe.pdf
```

P0 returned rc=0, and the listing in the test log put doe.pdf at 34471 bytes. The log opens with these version lines:

```text
pandoc 3.8.3
typst 0.15.1 (unknown commit)
pdftotext version 26.01.0
markitdown 0.1.5
```

T1 ran pdftotext with no options:

```bash
pdftotext doe.pdf out1.txt
```

Per the test log, T1 gave rc=0. The T1 output in the test log, out1.txt, shows the title Meeting notes twice, on lines 1 and 2. In that T1 output in the test log, every table cell got its own line, trailed by an empty one:

```text
Table
Item

Owner

Due

Draft
```

The two bullet lines in the T1 output in the test log start with a bullet character in place of the dash. In T1 of the test log, Friday and old lost the asterisks around them, and the last line with text holds a lone 1.

T2 added the layout option:

```bash
pdftotext -layout doe.pdf out2.txt
```

The test log gives T2 an exit code of rc=0. In the T2 output in the test log, out2.txt, the title shows up on lines 1 and 3, and each table row stays on one line, with spaces between the cells:

```text
                                 Item     Owner   Due
                                 Draft    Jane         Fri
                                 Review   John    Mon
```

T3 ran markitdown and saved what it printed:

```bash
markitdown doe.pdf > out3.md
```

For T3 the test log shows rc=0 and 0 stderr lines. In the T3 output in the test log, out3.md, the word Table sits on line 10, line 11 is empty, and line 12 reads Code. The T3 table cells in the test log come later, after See the guide. and a second Meeting notes line:

```text
Meeting notes

Item

Draft

Owner Due

Jane

Fri

Review

John Mon
```

So in T3 of the test log, Owner and Due share one line, and John and Mon share another.

## Markdown marks that did not come back

Once T3 was done, the test log counted markup in the three outputs. Its count line for each file reads:

```text
out1.txt heading=0 pipe=0 link=0 backtick=0 list=2 url=0
out2.txt heading=0 pipe=0 link=0 backtick=0 list=2 url=0
out3.md heading=0 pipe=0 link=0 backtick=0 list=2 url=0
```

The script counts lines that start with `#`, lines holding a pipe, lines holding `](`, lines holding a backtick, list lines that start with a dash and a space or a number and a dot, and lines naming example.com. Read against notes.md, the T1, T2 and T3 outputs in the test log show this:

- **Headings.** With heading=0 in T1 to T3 of the test log, the four heading lines came back as plain words, such as Meeting notes and Decisions.
- **Table.** With pipe=0 in T1 to T3 of the test log, no output holds a pipe or the dash row from notes.md.
- **Link.** The line `See [the guide](https://example.com/guide).` came back in T1 to T3 of the test log as the words See the guide. with no address, and url=0 for each file.
- **Code.** With backtick=0 in T1 to T3 of the test log, `print("hello")` came back as plain text, with no fence and no python label.
- **Quote.** The quote line came back in T1 to T3 of the test log as A quoted line. with no `>` in front.
- **Image.** The line `![Diagram](diagram.png)` came back in the test log as a caption line holding the words Figure 1 and Diagram, in the T1, T2 and T3 outputs alike.
- **Numbered list.** With list=2 in T1 to T3 of the test log, the two numbered lines kept their 1. and 2. markers. In the T1 to T3 outputs in the test log, the bullet lines start with a bullet character, so the list count leaves them out.

No output from T1 to T3 in the test log holds the `title:` key from the front matter of notes.md.

## A PDF with no text layer

The test also made scan.pdf, a PDF holding only a picture of a page. The script drew page 1 of doe.pdf as a picture named page.png. Then pandoc with typst turned a one-line file holding `![](page.png)` into scan.pdf, which the test log lists at 12673 bytes.

T4 and T5 ran the two commands on that file:

```bash
pdftotext scan.pdf out4.txt
markitdown scan.pdf > out5.md
```

In T4 of the test log, pdftotext returned rc=0, and out4.txt came to 4 bytes: the page number 1 followed by break characters. In T5 of the test log, markitdown returned rc=0 with 0 stderr lines, and out5.md also came to 4 bytes: the page number 1 followed by line breaks. Neither output in T4 or T5 of the test log held a word from the picture.

Look inside your own output file for text before you go further.

## When the file name is wrong

T6 pointed pdftotext at nofile.pdf, which does not exist in the test folder:

```bash
pdftotext nofile.pdf out6.txt
```

In T6 of the test log, pdftotext printed this line and returned rc=1:

```text
I/O Error: Couldn't open file 'nofile.pdf': No such file or directory.
```

A check after T6 in the test log found no out6.txt. T7 tried markitdown on the same missing name:

```bash
markitdown nofile.pdf
```

In T7 of the test log, markitdown returned rc=1. The last line it wrote to stderr reads:

```text
FileNotFoundError: [Errno 2] No such file or directory: 'nofile.pdf'
```

A script around either command can halt when the exit code is not 0.

## Rebuild the Markdown by hand

These steps are for your own file. The test log has two PDF inputs, doe.pdf and scan.pdf.

1. Keep the PDF open next to the text file, so you can see what each line was.
2. Mark each heading at its level. Heading marks are listed in [the Markdown cheat sheet](markdown-cheat-sheet.md).
3. Rebuild each table as a Markdown table. For the syntax, use [the post on Markdown tables](markdown-tables.md).
4. Put each link address back beside its link words.
5. Fence each code block and add its language label, as [the post on fenced code](markdown-code-blocks.md) shows.
6. Turn each quote back into lines that start with `>`.
7. If you have the picture file, swap the caption text for an image line. The image line form is in [a post on Markdown images](markdown-images.md).
8. Look for a title printed twice, a lone page number and bullet characters where your file needs dashes.
9. Read the result through, start to finish, against the PDF.

Have a .docx in place of a PDF? Pandoc handles that route in [the post on turning Word files into Markdown](word-to-markdown.md). Headed the other way, from Markdown to a PDF? [This post on making a PDF with pandoc and typst](markdown-to-pdf.md) has the command.

## Where Sashmark fits

Whatever pulled the text out, the Markdown file is what you go on editing, so the clean-up happens there. Sashmark, a .md reader and editor planned for desktop use, is in development and has no release so far. If plans hold, it would turn a .md file into a readable page, accept a fix typed straight onto that page, and put only that fix back in the file through its Round-Trip design. Changes View, another planned part, would mark lines an agent altered while you looked elsewhere, inside a repo or outside one. Nothing in the app itself is planned to use AI.

See the [planned features](/features), the [help pages](/help) and the [roadmap](/roadmap) for where each part stands.

[sashmark.com](https://www.sashmark.com/)
