A tool to download and organize images referenced in markdown files
Project description
Markdown Image Downloader
A Python package that automatically downloads and manages images referenced in markdown files, storing them locally in an _attachments folder. This script is particularly useful for maintaining local copies of images in markdown documentation and ensuring consistent image availability.
Or just for Obsidian's Readwise export, which I made this for.
Previously hosted on GitHub Gist. Moved here to allow for easier maintenance and contributions, if any. Also published to PyPI for convenience.
Requirements
Python 3.9+
Installation
Install directly from PyPI using pip:
pip install markdown-image-downloader
Usage
Run the package from the command line, providing the folder containing your markdown files as an argument:
markdown-image-downloader <folder_name>
Example
markdown-image-downloader ../Readwise/Articles
This will:
- Scan all markdown files in the
../Readwise/Articlesfolder - Download any images referenced in the markdown files
- Store them in
../Readwise/Articles/_attachments - Update the markdown files to reference the local copies
Features
- Uses custom HTTP headers to avoid download blocks
- Falls back to system
curlto bypass Cloudflare and other bot-detection challenges - Unwraps nested proxy CDN URLs (Substack, Omnivore, Microlink) to download from the original source
- Downloads images from URLs referenced in markdown files
- Creates local copies of images in an
_attachmentsdirectory - Automatically updates links in the markdown files with new local image paths
- Compresses large images to reduce storage space
- Supports multithreaded concurrent downloads
- Uses rate limit to prevent server overload and download blocks
- Progress bar for tracking download status
- Maintains detailed logging of error operations
- Sanitizes filenames for cross-platform compatibility
- Supports for rerunning the script without re-downloading images
- Correctly handles image URLs containing parentheses (e.g. thumbor
filters:focal(...))
How It Works
- Scanning: The script scans all
.mdfiles in the specified folder for image references, using a balanced-parentheses parser so that URLs with()inside them are captured correctly. - Downloading: For each image URL found:
- Unwraps known CDN proxy URLs (Substack, Omnivore, Microlink) to get the real origin URL
- Downloads the image if it's not already in
_attachments - Falls back to system
curlwhenurllibis blocked by Cloudflare / bot-protection (HTTP 403, 429, etc.) - Compresses images larger than 500KB while maintaining quality
- Generates unique filenames to avoid collisions
- Organization: Creates an
_attachmentsfolder to store all images - Updating: Updates markdown files to reference the local copies in
_attachments
Features in Detail
Image Compression
- Automatically compresses large images
- Maintains reasonable quality through progressive compression
- Converts RGBA images to RGB with white background
Filename Handling
- Preserves original filenames
- Sanitizes filenames for cross-platform compatibility
Concurrent Processing
- Uses ThreadPoolExecutor for parallel downloads
- Includes progress bar for tracking downloads
- Implements rate limiting to prevent server overload
Error Handling
- Comprehensive logging of all operations
- Graceful handling of download failures
- Skips already processed images
Logging
The script creates detailed logs in a logs directory:
- Location:
./logs/image_downloader.log - Includes timestamps, operation details, and error messages
- New log file created for each run
Limitations
- Only processes image links in markdown format:
 - Requires internet connection for downloading external images
- May be rate-limited or just straight denied by some servers (Cloudflare CAPTCHA challenge, Vercel bot detection, etc.)
- SVG files are downloaded but not compressed
- 404 errors are not retried (the image genuinely no longer exists)
Contributing
Feel free to submit issues, fork the repository, and create pull requests for any improvements.
License
This project is available under the MIT License.
Publishing to PyPI (Maintainer Guide)
This project uses uv for building and publishing. uv handles everything — no need for twine or python -m build separately.
Prerequisites
uvinstalled (brew install uvon macOS)- A PyPI API token stored in
~/.pypircor passed viaUV_PUBLISH_TOKENenv variable
~/.pypirc format
[pypi]
username = __token__
password = pypi-<your-token-here>
Steps
1. Make your code changes.
2. Bump the version in pyproject.toml:
[project]
version = "0.1.X" # increment this
3. Build the wheel and source distribution:
uv build
This creates a dist/ folder with *.whl and *.tar.gz files.
4. Publish to PyPI:
uv publish
uv reads credentials from ~/.pypirc automatically, or you can pass a token directly:
uv publish --token pypi-<your-token>
5. Commit the version bump and tag the release:
git add pyproject.toml
git commit -m "Bump to version 0.1.X"
git tag v0.1.X
git push origin main --tags
6. Verify on PyPI: https://pypi.org/project/markdown-image-downloader
One-liner (after token is configured)
uv build && uv publish
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file markdown_image_downloader-0.1.10.tar.gz.
File metadata
- Download URL: markdown_image_downloader-0.1.10.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e946b91100483586ae982040a40c16c3b4fdd2ba8c9c333fdb667c1f0b0eab17
|
|
| MD5 |
d7ebba8ca5bb01738c8f4d8f4902aaaa
|
|
| BLAKE2b-256 |
f3c1f22c0579ba6005a9f5bb7d292cd6092d45f822950af53aec5f9ca8371e92
|
File details
Details for the file markdown_image_downloader-0.1.10-py3-none-any.whl.
File metadata
- Download URL: markdown_image_downloader-0.1.10-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d4a770ca2f122b4321dc716c6981b8f77264b5c5f09f9a20ff5e31bde14899c
|
|
| MD5 |
b8dd06359609818a198bdc704e51c09d
|
|
| BLAKE2b-256 |
356173832fd3f8ecaf7b6de33a7731c10b4ae63fae7c4a9ec7f8715e3b3603e8
|