Image to Markdown OCR for Article Screenshots: Strip the Page, Stitch the Scroll
If you searched for image to markdown OCR for article screenshots, you probably have a folder of captured news stories, blog posts, or newsletter issues and want each one as a Markdown file you can read, search, quote, or feed to a RAG index. Short answer: OCR the screenshot to Markdown, then run a small cleanup step before you trust it. An article screenshot is not just the article. It carries the browser address bar, the site navigation, ads, a cookie banner, and often a second or third capture of the same page because the story did not fit on one screen. OCR reads all of that faithfully. Your job is to keep the story, drop the page around it, and record where it came from.
This post is the screenshot-specific version of the job. For layout-aware parsing of scans and photos in general, read JPG to Markdown with visual document parsing. For choosing between plain text, Markdown, and JSON responses, read the image to text API guide.
When OCR is the wrong tool for an article
Check this first, because it saves the most time. If you still have the live URL and the page loads, you do not need OCR at all. Fetch the HTML and convert it. Firefox’s Reader View uses an article extractor that Mozilla publishes as a standalone library, Readability, and there are plenty of HTML-to-Markdown converters downstream of it. That path keeps the hyperlinks, the image URLs, and the exact characters, which no screenshot can give you.
OCR on an article screenshot earns its place when the pixels are all you have:
- A paywalled or deleted page that someone captured before it changed
- Screenshots shared in chat, email, or a support ticket, with no link attached
- Research and monitoring archives where the capture itself is the record of what the page said on a given day
- Apps and newsletter clients that do not expose the underlying HTML
What came back from a real article screenshot
To show what the cleanup step has to handle, we built a synthetic article page (a fictional newspaper, a fictional reporter, a fictional story about digitizing tide ledgers) and saved it as a 1100 px wide PNG. It has a browser address bar, a dark navigation bar, a section label, a two-line headline, a byline, body paragraphs, an ad box, two subheadings, a bullet list, a pull quote, a related-story link, and a cookie banner across the bottom.
We uploaded it to OCRskill’s Markdown endpoint with a free key from get-key.json. POST /ocr accepts PNG, JPEG, WebP, GIF, BMP, and TIFF images, plus PDF, up to 20 MB per request.
curl https://api.ocrskill.com/get-key.json
export API_KEY="sk-your-key-here"
curl https://api.ocrskill.com/ocr \
-H "Authorization: Bearer $API_KEY" \
-F "file=@article-full.png" > full.md
The response, lightly shortened in the middle:
https://example-harbor-gazette.test/2026/10/tidal-records
# THE HARBOR GAZETTE
News | Business | Science | Opinion | Subscribe
---
## SCIENCE
# Volunteers Digitize a Century of Tide Records
By Ana Example | October 12, 2026 | 6 min read
A group of retired engineers has spent two winters scanning handwritten tide ledgers kept at the old harbor office between 1904 and 2003. The ledgers record water height four times a day.
...
---
**ADVERTISEMENT**
Harbor Tours: book your autumn cruise today
---
## How the scanning works
Each ledger is photographed on a copy stand, then the images are checked by a second volunteer. The team follows three rules:
- Photograph every page, even blank ones.
- Never flatten a binding that is already cracked.
- Log the shelf number before the book leaves the room.
> "The handwriting changes every few years, and you can tell when a new clerk started," said one volunteer.
...
Related: Storm surge maps from 1953 are back online
---
We use cookies to improve your experience.
Accept all
The article itself came through well. The headline is an # heading, the subheadings are ##, the bullets are a real list, and the pull quote is a blockquote. A word the screenshot hyphenated across two lines (“climate-” and “controlled”) came back joined as climate-controlled. The curly quotes in the pull quote came back as straight quotes, which is worth knowing if you plan to compare the text against the original.
The problems are all about the page, not the reading:
- Two
#headings. The masthead “THE HARBOR GAZETTE” is also an#heading, because visually it is big and bold. The section label “SCIENCE” became a##heading, the same level as the article’s real subheadings. - Chrome kept as content. The address bar URL, the navigation bar, the ad label and ad copy, and the cookie banner and its button text are all in the output.
- Extra rules. OCR added
---horizontal rules between the visual blocks it saw. - No link targets. “Related: Storm surge maps from 1953 are back online” survived as text. Its URL did not, because a screenshot only contains what was drawn on screen. The same goes for every inline link in the body.
None of that is an OCR error. A transcript of the screenshot should include the nav bar. It is a cleanup job, and it is predictable enough to automate.
Long articles: overlapping scroll captures
Most real articles are longer than one screen, so people capture them in pieces. We split the same page into two captures with about 260 px of overlap, the way you get them by scrolling and taking a second screenshot, and ran each one through /ocr.
The overlap showed two things a naive merge would get wrong. The first capture ended on a bullet that the crop edge clipped, and it came back as - Photograph every page, even blank ones without its final period. And the shared region was not formatted identically in both captures: the first returned **ADVERTISEMENT** with plain ad copy, while the second returned a plain ADVERTISEMENT with bold ad copy. Matching repeated lines by exact string fails on both counts. Match on the words instead, and when the two captures disagree about a line in the overlap, keep the longer reading.
A runnable cleaner for article screenshot Markdown
The script below takes one or more /ocr Markdown files from the same article, in top-to-bottom capture order. It drops browser and site chrome, stitches overlapping captures, picks the article headline, moves the byline and source URL into front matter, and writes clean Markdown to stdout. It needs Python 3.10 or newer and nothing outside the standard library.
import json
import re
import sys
from datetime import date
URL = r"^https?://\S+$" # address bar captured in the screenshot
CHROME = [
URL,
r"cookies?\b.*(experience|consent|privacy)",
r"^(accept|reject)( all)?$",
r"^-{3,}$", # horizontal rules OCR adds between visual blocks
r"^([\w&' ]{2,20} \| ){3,}[\w&' ]{2,20}$", # nav bars: "News | Business | ..."
]
AD_LABEL = r"^(\*\*)?(advertisement|sponsored)(\*\*)?$"
def norm(line: str) -> str:
"""Compare lines by their words, not their Markdown decoration."""
line = re.sub(r"^(#+|>|[-*]|\d+\.)\s*", "", line.strip())
line = re.sub(r"[*_`\"'\u201c\u201d]", "", line)
return re.sub(r"[^\w]+", " ", line).strip().lower()
def strip_chrome(md: str) -> list[str]:
kept, skip_ad = [], False
for line in (l.strip() for l in md.splitlines()):
if not line:
continue
if skip_ad: # the line right after an ad label is the ad copy
skip_ad = False
continue
if re.match(AD_LABEL, line, re.I):
skip_ad = True
continue
if not any(re.search(p, line, re.I) for p in CHROME):
kept.append(line)
return kept
def stitch(a: list[str], b: list[str]) -> list[str]:
"""Join two overlapping captures of the same page, top one first."""
na, nb = [norm(x) for x in a], [norm(x) for x in b]
for size in range(min(len(a), len(b)), 0, -1):
head, edge = na[-size:-1], na[-1]
# The last line of the top capture may be cut off at the crop edge.
if head == nb[: size - 1] and nb[size - 1].startswith(edge):
overlap = [max(x, y, key=len) for x, y in zip(a[-size:], b[:size])]
return a[:-size] + overlap + b[size:]
return a + ["<!-- no overlap found: check capture order -->"] + b
def to_markdown(lines: list[str], url: str | None) -> str:
h1 = [i for i, l in enumerate(lines) if l.startswith("# ")]
title_i = max(h1, key=lambda i: len(lines[i])) if h1 else -1
title = lines[title_i].lstrip("# ").strip() if h1 else ""
body = lines[title_i + 1 :] # masthead and section kicker sit above the headline
byline = body.pop(0) if body and body[0].lower().startswith("by ") else ""
out = ["---", f"title: {json.dumps(title)}", f"byline: {json.dumps(byline)}",
f"source_url: {url or ''}", f"captured: {date.today().isoformat()}",
"---", "", f"# {title}"]
for prev, line in zip([""] + body, body):
if not (line.startswith("- ") and prev.startswith("- ")):
out.append("") # blank line between blocks, none inside a list
out.append(line)
return "\n".join(out) + "\n"
if __name__ == "__main__":
raw = [open(p, encoding="utf-8").read() for p in sys.argv[1:]]
url = next((m.group(0) for r in raw for m in [re.search(URL, r, re.M)] if m), None)
merged = strip_chrome(raw[0])
for nxt in raw[1:]:
merged = stitch(merged, strip_chrome(nxt))
print(to_markdown(merged, url), end="")
Run it on the two scroll captures:
python3 clean_article.py part1.md part2.md > tide-records.md
The start of the result:
---
title: "Volunteers Digitize a Century of Tide Records"
byline: "By Ana Example | October 12, 2026 | 6 min read"
source_url: https://example-harbor-gazette.test/2026/10/tidal-records
captured: 2026-10-14
---
# Volunteers Digitize a Century of Tide Records
A group of retired engineers has spent two winters scanning handwritten tide ledgers kept at the old harbor office between 1904 and 2003. The ledgers record water height four times a day.
The clipped bullet came back with its period, the ad, nav, and cookie banner are gone, there is one # heading, and the overlap appears once. Running the same script on the single full-page capture produced identical Markdown except for one line: the related-story link was bold in the second scroll capture and plain in the full-page one. That is the kind of difference to expect between captures, and it is harmless here.
Treat the script as a starting point. The chrome patterns match this test page, so add the cookie wording, ad labels, newsletter sign-up boxes, and share-button text your sources actually show. “Longest # heading wins” is a heuristic that fits most news layouts and will pick wrongly on a page with a long site tagline. If a capture order is wrong or two screenshots do not overlap, the script leaves an HTML comment in the output instead of guessing, so search for it before you index anything.
Capture tips that make the Markdown better
Most of the quality is decided before the upload:
- Capture at native resolution. Do not downscale screenshots before upload. Small body text in a heavily compressed JPEG is where character errors start. PNG is a better default for screenshots than JPEG.
- Crop to the article column when you can. A crop that excludes the sidebar removes “Most read” lists and recommendation widgets that look like body text and are hard to filter afterward.
- Overlap scroll captures generously. Two or three full lines of shared text give the stitcher something to match. Captures that touch exactly at the edge lose or duplicate a line.
- Dismiss the cookie banner first. It covers body text along the bottom of the screen, and no cleanup step can recover words that were hidden in the pixels.
- Prefer a full-page capture. Many browsers and screenshot tools can capture the whole scrolling page as one tall image. One image means no stitching. Keep it under the 20 MB request limit.
- Keep the address bar in frame, or record the URL separately. It is the only reliable source for
source_urlin the front matter.
Where it fits in a pipeline
For a screenshot archive or a research corpus, keep each step simple and keep the original:
- Store the image. Keep the screenshot unchanged, with its content hash and capture time.
- OCR to Markdown. Call
POST /ocrand save the raw response beside the image. That raw file is your evidence of what the page said, chrome included. - Clean and stitch. Run the cleaner to produce the reading copy, with the source URL and capture date in front matter.
- Index the clean copy. Chunk on the
##headings for RAG, and keep a pointer back to the image so any quoted sentence can be checked against the pixels. - Route oddities to a person. Empty output, a missing headline, the no-overlap comment, or a byline the script did not find are cheap signals that a capture needs a look.
If the screenshots already sit in object storage, paid keys can send a public HTTPS document URL instead of uploading bytes. Free keys return 403 for that mode. When the goal is a training corpus rather than a reading archive, the OCR for AI training data guide covers dedupe and filtering at that scale. When you need named values from each capture, such as a publication date or an author field, move that step to POST /ocr.json with an explicit field list instead of parsing it out of Markdown.
Limitations to plan for
- Links are gone. A screenshot has link text and no targets. If links matter, you need the HTML.
- Images become placeholders or nothing. Photos inside the article are not described. Captions come through as text when they are visible.
- Characters are normalized. In our test, curly quotes came back straight and a hyphenated line break was joined. That is usually what you want for reading, and it means the Markdown is not a byte-exact copy of the published text.
- Heading levels follow the look of the page. A big bold masthead reads as a heading. The cleaner fixes the common case and will not fix every layout.
- Multi-column and magazine layouts with text wrapped around images can change reading order. Check a sample from each publication before you batch it.
- Rights are not an OCR question. Converting a screenshot does not change who owns the article. Keep copies for the purposes your situation allows, and if you are publishing or redistributing excerpts, the US Copyright Office’s fair use index is a place to start reading, not a substitute for advice.
Measure on your own captures before you promise anything. Take twenty screenshots from the sources you actually archive, run the full pipeline, and count how many clean files still need a manual fix and what kind: a stray widget, a wrong headline, a broken stitch, or a misread word. The answer tells you whether to add chrome patterns, change how people capture, or keep a review step.
Conclusion
Image to Markdown OCR for article screenshots is two jobs. The OCR step reads the screenshot faithfully, and in our test it got the article’s headings, lists, and quotes right. The cleanup step decides what counts as the article: one headline instead of two, no nav, ads, or cookie text, overlapping captures merged once, and a source URL and capture date in front matter, because the screenshot has no links of its own. Keep the raw OCR output as the record and the cleaned file as the reading copy, and fall back to the HTML whenever the live page is still there.
To try it, get a key from get-key.json, run two overlapping captures of a real article through /ocr, and pass both files to the cleaner above. Product details and pricing are at ocrskill.com.
