📖 Documentation: English · 中文
A small, dependency-free Python CLI that downloads remote images referenced in your
Obsidian Markdown notes (e.g. saved via the Obsidian Web Clipper) into local
attachments/ folders, and rewrites the links to relative paths — so your notes work
fully offline.
- Local / already-offline images: left untouched.
- Already-localized images: not re-downloaded on re-run (idempotent).
- Downloads + rewrites only — never deletes your files.
- Links inside code blocks / inline code are ignored, so documented examples are not mistaken for real images.
| Item | Requirement |
|---|---|
| OS | macOS / Windows / Linux |
| Python | 3.6+ (3.8+ recommended) |
| Dependencies | None — standard library only, no pip install |
| Network | Must be able to reach the image URLs |
macOS / Linux — Python 3 is usually preinstalled. Verify:
python3 --versionWindows — Download from python.org; tick "Add Python to PATH" during install. Then:
python --version
⚠️ On Windows the command ispython, notpython3.
# macOS / Linux
python3 localize_images.py
# Windows
python localize_images.pyBy default it processes the current directory (and all sub-directories).
Process a specific directory:
python3 localize_images.py /path/to/your/vault| Option | Description |
|---|---|
root |
Root directory to scan (default: current directory) |
--dry-run |
Preview only — list what would be downloaded, change nothing |
--workers N |
Concurrent downloads (default 8; raise to 16 for many images) |
--no-rewrite |
Download only, don't rewrite the markdown (debug) |
--referer URL |
Custom Referer header to bypass hotlink protection |
--exclude PATH |
Exclude a file or folder (incl. subfolders); repeatable; also reads .imagelocalizerignore |
Skip notes you don't want touched — e.g. a folder of private notes, or one specific file. Folders are skipped entirely, including their subfolders:
python3 localize_images.py --exclude 010Finance/SeekingAlpha --exclude "Private Notes.md"You can also drop a .imagelocalizerignore file (gitignore-style, # comments) in the
root; each line is a path to exclude. Excluded folders (and their subfolders) are
skipped as a whole. A ready-to-edit template lives at
.imagelocalizerignore.example — copy it to
.imagelocalizerignore and adjust the rules.
- Preview first (safe, changes nothing):
python3 localize_images.py --dry-run
- Run for real (downloads + rewrites links):
python3 localize_images.py
- Open Obsidian and confirm images render.
- Walks the root directory for every
.mdfile (excluded items are skipped entirely). - Parses image links from three sources — after stripping code blocks and inline
code so examples aren't mistaken for real images:
- Standard Markdown:
 - HTML tags:
<img src="https://..."> - Inline base64:
data:image/png;base64,...
- Standard Markdown:
- Downloads
http/httpsanddata:images into the note's siblingattachments/folder; an MD5 hash is appended to the filename to avoid collisions. - Rewrites each link to the relative
attachments/name.pngpath. - Failed downloads retry 3×; still-failing ones are skipped with a warning.
- Always
--dry-runfirst. - Revert anytime: if your vault is Git-tracked (Obsidian Git), just
git checkout. - Hotlink 403: some CDNs block requests without a referer — re-run with
--referer https://www.douban.com/(or the source site). - 404 dead link: the source URL itself is broken/invalid — the script can't fix it; manually correct or delete that image line.
- Idempotent: after the first run links become local, so a second run finds nothing.
- Chinese paths / filenames: fully supported (Python 3 native Unicode).
- Where do images go? Into an
attachments/folder next to each note (relative paths, which Obsidian resolves on every platform).
| Symptom | Cause | Fix |
|---|---|---|
'python3' is not recognized (Windows) |
PATH not set / used python3 |
Use python; re-install with "Add to PATH" |
Many IncompleteRead |
Unstable network / rate limit | Auto-retried; re-run later if needed |
HTTP 403 |
Hotlink protection | Re-run with --referer |
HTTP 404 |
Source link dead | Fix the URL or delete the image line |
| Images downloaded but not shown | Links not rewritten | Don't use --no-rewrite; run again |
MIT © 2026 stevenlii