EzImage is a image uploader tool deeply customized for VS Code. It is designed to solve the pain points of "hard to save, annoying to paste" in Markdown creation, helping you upload images and insert Markdown links in the most natural way (clipboard paste, drag and drop, context menu).
⚠️ Compression not working or upload looks broken? Check the 🔧 Troubleshooting section — most reported issues (sharp failing to load, public URL 404, missing R2 fields) have a known fix there.
✨ Features
- 🚀 Multi-Platform Support: Pluggable architecture, starting with Cloudflare R2, with AWS S3, Aliyun OSS, and Tencent COS coming soon.
- 📸 Ultimate Pasting Experience:
- Clipboard Upload: Press
Cmd+Alt+V (Mac) or Ctrl+Alt+V (Win/Linux) to instantly upload and insert.
- Smooth Drag & Drop: Drag images directly from your folder into the editor for automatic uploading.
- 🔄 Local Images → Cloud URLs: Right-click → EzImage: Convert Local Image Paths to URLs to scan every
 in the current Markdown document, upload them to your bucket, and replace the path in place — all in one atomic undo step.
- 📉 Intelligent Image Engine: Powered by the industrial-grade
sharp engine.
- Automatically convert images to WebP for maximum compression while maintaining quality.
- Supports automatic resizing (Max Width) and quality control.
- Zero-config setup: on first use, EzImage detects whether
sharp is available and offers a one-click install of the native binary that matches your OS and Node.js ABI. If you prefer to skip compression, just toggle ezimage.compress off.
- 📂 Flexible Naming: Support rich template variables like
{yyyy}/{MM}/{timestamp}-{random}.{ext} to avoid file name conflicts.
- 📋 Deep Integration: Provides editor context menus for a natural workflow.
🚀 Multi-IDE Support
EzImage not only supports standard VS Code, but also perfectly adapts to current mainstream AI code editors:
- Antigravity: Deeply adapted, the preferred environment for AI-assisted development.
- Cursor: Native compatibility with excellent performance.
- Windsurf: Full feature support.
- Trae: Perfectly compatible, powered by ByteDance.
- VSCodium: Also works for the open-source version of VS Code.
📦 Installation
Method A: Install from VSIX (Recommended)
- Download the latest
.vsix file from GitHub Releases. Look for the file named ezimage-X.Y.Z.vsix under the latest release.
- Press
Cmd+Shift+P (Mac) / Ctrl+Shift+P (Win) in your editor and search for Install from VSIX.
- Or use the command line, replacing
ezimage-X.Y.Z.vsix with the actual filename you downloaded:
| IDE / Environment |
Command |
| VS Code |
code --install-extension ezimage-X.Y.Z.vsix |
| Antigravity |
antigravity --install-extension ezimage-X.Y.Z.vsix |
| Cursor |
cursor --install-extension ezimage-X.Y.Z.vsix |
| Windsurf |
windsurf --install-extension ezimage-X.Y.Z.vsix |
| Trae |
trae --install-extension ezimage-X.Y.Z.vsix |
Method B: From Marketplace
- Search for
EzImage in the extension marketplace and install it with one click.
- Or visit the VS Code Marketplace to install.
⚙️ Configuration
After installation, follow these steps to configure:
- Press
Cmd+Shift+P (Mac) / Ctrl+Shift+P (Win) to open the command palette.
- Search and run
EzImage: Open Settings.
- Configure your storage service (e.g., Cloudflare R2):
- Provider:
r2
- Account ID: Your Cloudflare Account ID
- Bucket Name: Your storage bucket name
- Access Key ID / Secret Access Key: R2 API credentials
- Public URL: Your bucket's public distribution URL
All settings
Settings are listed in the order they appear in VS Code's Settings panel, which follows an explicit order property in package.json. Within each group, the read order matches the most common user workflow.
Storage — where uploaded images go
| Key | Default | Description |
| :--- | :--- | :--- |
| ezimage.provider | r2 | Storage provider. Currently only r2 is wired in; S3/OSS/COS are roadmap items. |
| ezimage.r2.accountId | "" | Cloudflare Account ID. |
| ezimage.r2.accessKeyId | "" | R2 access key ID. |
| ezimage.r2.secretAccessKey | "" | R2 secret access key. |
| ezimage.r2.bucketName | "" | R2 bucket name. |
| ezimage.r2.publicUrl | "" | Public distribution URL, e.g. https://pub-xxxx.r2.dev. |
| ezimage.pathTemplate | {yyyy}/{MM}/{timestamp}-{random}.{ext} | Path template for uploaded objects. Available variables: {yyyy} {MM} {dd} {hh} {mm} {ss} {timestamp} {random} {name} {ext}. |
Compression — what happens to images before upload
| Key | Default | Description |
| :--- | :--- | :--- |
| ezimage.compress | true | Enable WebP compression before upload. |
| ezimage.maxWidth | 1920 | Maximum width in pixels; images wider than this are downscaled before encoding. Set to 0 to keep original dimensions. |
| ezimage.quality | 85 | WebP quality (1–100). Higher = larger files, better fidelity. |
| ezimage.autoInstallSharp | true | When sharp is missing, prompt to install the native binary on first use. Requires Node.js ≥ 18.17 and npm on PATH. |
| ezimage.disableCompressionNotice | false | Suppress the warning shown when the sharp compression engine is unavailable. |
Insert format — how the snippet appears in your Markdown
| Key | Default | Description |
| :--- | :--- | :--- |
| ezimage.insertFormat | markdown | How to render the inserted snippet. One of markdown / html-wrap / html-figure / custom. See Insert format templates. |
| ezimage.insertWidth | 100% | Width attribute used in HTML templates ({width}). e.g. 65%, 600px, auto. Empty string omits the attribute. |
| ezimage.insertAlign | none | Alignment for HTML templates. none skips the wrapper <div>. |
| ezimage.insertCustomTemplate | "" | Custom template used when insertFormat = custom. Variables: {url} {filename} {name} {ext} {width} {align} {alt}. |
| ezimage.insertIncludeName | true | Use the source filename as alt text / figcaption. |
Language — display language of prompts and info messages
| Key | Default | Description |
| :--- | :--- | :--- |
| ezimage.language | auto | Display language for EzImage prompts, errors, and info messages. auto follows VS Code's display language. Settings panel labels themselves always follow VS Code (not this setting). |
Three built-in presets cover the most common shapes:
| Format |
With insertAlign = center, insertWidth = 100% |
insertAlign = none |
markdown |
 |
(same) |
html-wrap |
<div align="center"><img src="…" alt="photo" width="100%"></div> |
<img src="…" alt="photo" width="100%"> (no wrapper) |
html-figure |
<figure><img src="…" alt="photo" width="100%"><figcaption>photo</figcaption></figure> |
(same — figure ignores insertAlign) |
For full control, set ezimage.insertFormat = custom and define ezimage.insertCustomTemplate with any combination of variables:
| Variable |
Resolves to |
{url} |
Public R2 URL after upload |
{filename} |
Full filename including extension, e.g. photo.png |
{name} |
Filename without extension, e.g. photo |
{ext} |
Extension without dot, e.g. png |
{width} |
ezimage.insertWidth value (empty → no width attribute) |
{align} |
ezimage.insertAlign value |
{alt} |
Alt text (filename, or insertCustomAlt when set) |
Unknown placeholders are left in place so a typo in your template stays visible instead of being silently dropped.
One-shot override: the command EzImage: Upload Clipboard Image As… lets you pick a different format for a single upload without changing the default. Bind it to a shortcut via File → Preferences → Keyboard Shortcuts → search "ezimage.uploadClipboardAs" if you want a dedicated key (e.g. Ctrl+Alt+Shift+V) for HTML-centered inserts.
R2 setup walkthrough
For a step-by-step guide with screenshots covering Cloudflare R2 token creation, bucket configuration, and connecting it to EzImage, see 📘 Cloudflare R2 Setup Guide.
For path template variables in detail, see 📝 Path Variables Manual.
🔄 Local Images → Cloud URLs
When you've been drafting locally and want to publish, you don't have to upload each image by hand. EzImage can scan your Markdown file, upload every local image to the bucket, and rewrite the references in place — all in one go.
Quick start
- Open
Settings (Cmd+, / Ctrl+,) → search for EzImage.
- Enable
ezimage.localImageUpload.enabled. This adds two entries to the editor right-click menu under Modification:
- EzImage: Convert Local Image Paths to URLs — scans the entire file.
- EzImage: Convert Local Image Paths in Selection — scans only the current selection (appears only when something is selected).
- Right-click anywhere in a Markdown file → EzImage: Convert Local Image Paths to URLs.
- EzImage finds every
, shows a confirmation like "Found 5 local image(s) (2 skipped). Upload and replace in-place?", then uploads serially with a progress notification.
- The whole conversion lands as one atomic edit — press
Cmd+Z once to roll it all back.
What gets scanned
- ✅ Markdown inline images:
, , , .
- ✅ Relative paths (resolved from the Markdown file's directory) and absolute paths.
- ✅ The same image referenced multiple times — each reference is processed independently (same source → potentially multiple cloud URLs, since cloud keys are randomized by
ezimage.pathTemplate).
What gets skipped (with reasons)
| Reason |
Example |
Behaviour |
| Remote URL |
 |
Skipped silently. |
| Data URI |
 |
Skipped silently. |
| Non-image file |
 |
Skipped silently. |
| File not on disk |
 |
Skipped silently by default. Disable ezimage.localImageUpload.skipNonExistent to surface as an error. |
The confirmation dialog tells you how many were skipped so nothing is silently dropped without context.
Limitations (v1)
- Only Markdown inline images are matched. Reference-style (
![alt][ref]) and HTML <img> tags are intentionally out of scope for v1.
- Only local workspaces (file scheme). Remote workspaces (WSL, SSH, Dev Containers) are not yet supported.
- Untitled (unsaved) documents are rejected — save the file first so relative paths can be resolved.
- The conversion is manual only: it never runs on save, on paste, or otherwise behind your back. Enable the context-menu entry, trigger it, watch it work, undo if you regret it.
Settings
| Setting |
Default |
What it does |
ezimage.localImageUpload.enabled |
false |
Show "EzImage: Convert Local Image Paths to URLs" in the editor context menu. Off by default — opt in once and forget about it. |
ezimage.localImageUpload.skipNonExistent |
true |
Skip images whose local file is missing (deleted). Set false to surface them as hard errors instead. |
🖱 Drag-and-Drop Upload
Opt-in: takes over drag-and-drop for image files in Markdown editors. When enabled, dropping one or more images into a .md file uploads them to the cloud and inserts the URLs in place of the local paths.
⚠️ You must hold Shift while dragging. VS Code's built-in editor-drop behaviour requires Shift — without it the file opens in a new tab. This is a VS Code design choice, not something EzImage controls. The status bar shows a Hold Shift to drop into editor hint while you drag.
Quick start
- Settings (
Cmd+, / Ctrl+,) → search for EzImage.
- Enable
ezimage.dropUpload.enabled.
- In a Markdown file, hold
Shift, drag one or more images from Finder / Explorer into the editor.
- Confirm with Upload.
- Each image first appears as
, then morphs into  once the upload completes.
- A toast summarises:
EzImage: drop complete — 3 uploaded, 0 failed. · saved 1.4 MB.
What gets uploaded
- ✅ Pure image drops: one or more
*.png / *.jpg / *.gif / *.webp / *.svg.
- ❌ Mixed drops (e.g.
foo.png + notes.txt): EzImage backs off entirely and lets VS Code handle the drop natively. We never silently swallow a non-image file the user dropped alongside images.
- ❌ Non-Markdown files: EzImage's drop handler doesn't activate, so other languages keep their native drop semantics.
Settings
| Setting |
Default |
What it does |
ezimage.dropUpload.enabled |
false |
Take over drag-and-drop for image drops in Markdown editors. Off by default so your existing drop behaviour is unchanged until you opt in. |
ezimage.dropUpload.requireConfirm |
true |
Pop a confirmation dialog before uploading. Disable to skip the prompt and upload immediately (faster, but a misclick triggers a network call). |
ezimage.dropUpload.placeholder |
uploading… |
Alt text shown while the upload is in flight. If the upload fails, the placeholder is left as a still-valid (local-path) Markdown link rather than erased. |
FAQ: why does it still open the file in a new tab?
This is VS Code's safety mechanism, not an EzImage bug. VS Code only treats a drag as "drop into the editor" when you hold Shift; without Shift it interprets the gesture as "open this file in a new editor tab" and calls its built-in file opener — completely bypassing every DocumentDropEditProvider, ours included. There is no extension-level setting that disables this behaviour; the only control surface is upstream in VS Code itself. The fix is just muscle memory: drag + Shift.
⌨️ Hotkeys
| Action |
Mac Hotkey |
Windows/Linux Hotkey |
| EzImage: Upload Clipboard Image |
Cmd + Alt + V |
Ctrl + Alt + V |
| EzImage: Pick & Upload Image Files |
Search command EzImage: Pick & Upload Image Files |
|
🌐 Languages
EzImage ships with English and Simplified Chinese translations. The display language is controlled by ezimage.language:
| Value |
Behaviour |
auto (default) |
Follows VS Code's display language. Chinese-locale users see 简体中文; everyone else sees English. |
en |
Force English regardless of VS Code locale. |
zh-CN |
Force Simplified Chinese. |
Language changes take effect immediately — no reload needed.
What gets translated
Only the runtime UI that EzImage generates itself — prompts, error dialogs, the Output channel, the format-picker menu. Concretely:
- ✅ All
vscode.window.showErrorMessage / showWarningMessage / showInformationMessage text
- ✅ All
vscode.window.showQuickPick labels and descriptions (e.g. the "Insert this upload as…" picker)
- ✅ All text written to the Output → EzImage channel
- ✅
vscode.window.withProgress titles and progress messages
- ❌ The labels and descriptions in the Settings panel itself
Why the Settings panel stays in English (or VS Code's language)
VS Code reads those strings from package.json and picks a translation only from the VS Code Marketplace's own package.nls.<locale>.json files, keyed by the VS Code display language, not the per-extension setting. There is no public API for an extension to override those strings at runtime.
In practice this is fine: the Settings panel labels describe what a setting does ("Maximum width for compressed images"), not when something failed. The user-facing runtime messages — which the user actually sees while uploading — are what ezimage.language controls.
If you want the entire VS Code UI in Chinese (including EzImage's settings panel), run Configure Display Language from the command palette and pick 中文 (简体). That's a VS Code-level switch, not EzImage's.
Adding a new translation
- Copy
l10n/bundle.json to l10n/<locale>.bundle.json and translate the values (not the keys).
- In
src/i18n.ts, add the new locale code to SupportedLocale and BUNDLES.
- Update
src/types.ts / package.json enum so the language setting surfaces in the UI.
- CI validates key parity automatically —
npm test catches drift between bundles.
Translations are JSON, so no special tooling is needed. PRs welcome.
🔧 Troubleshooting
Images upload as the original file (WebP compression not working)
This is the most common issue and almost always means the sharp native module failed to load inside VS Code's bundled Node.js. Open the Output → EzImage panel (View → Output → EzImage) and look for one of these lines:
Compression engine unavailable: <error> — sharp was found on disk but couldn't be loaded. The most likely cause is an Electron/Node ABI mismatch: VS Code ships with its own Node.js version, and the prebuilt binary you have may not match it. Click "Reload Window" after npm install sharp succeeds, or set ezimage.autoInstallSharp: false and reinstall manually with npm rebuild sharp.
Could not find npm on PATH — sharp could not be auto-installed because npm isn't available. Install Node.js ≥ 18.17 from nodejs.org (which includes npm), then click "Install sharp" again.
Auto-install exited with code N — npm install itself failed. The Output panel will include a hint tailored to the failure (network/permissions/proxy). Common fixes:
- Corporate proxy: set
NPM_CONFIG_REGISTRY in your environment to your internal mirror.
- Permission denied: the extension folder may be read-only. Reinstall the VSIX or move VS Code's extensions directory to a writable location.
- Network timeout: run
npm install sharp manually in a terminal from inside the extension folder (~/.vscode/extensions/kiang.ezimage-X.Y.Z/) and reload VS Code when it finishes.
If you previously chose "不再提示" (don't ask again), reset the flag with ezimage.autoInstallSharp: true in settings, then trigger any upload command.
If you just want to keep working without compression, set ezimage.compress: false — uploads will continue to work, just without WebP conversion.
Upload succeeds but image URL returns 404
The object was uploaded but ezimage.r2.publicUrl doesn't match the bucket's actual public distribution URL. Check the Custom Domains tab of your R2 bucket and copy the Public Bucket URL (https://pub-xxxx.r2.dev) into the setting. Make sure the bucket itself has public access enabled.
Missing R2 Access Key ID (or another "Missing R2 …" error)
Run EzImage: Open Settings and fill in the field mentioned in the error. All five R2 fields (accountId, accessKeyId, secretAccessKey, bucketName, publicUrl) are required.
Cmd/Ctrl+Alt+V doesn't paste anything
The command only fires inside Markdown files. Open a .md document, place the cursor where you want the image link, then press the shortcut. If you're still on macOS but pasted a Finder file (not a screenshot), the helper should detect the file reference and offer it; if it doesn't, check the Output → EzImage channel for the line Using image path from clipboard text or Unable to read clipboard file reference.
🗺️ Roadmap
- [x] Cloudflare R2 basic support
- [x] Multi-format auto-conversion to WebP compression (with one-click
sharp install since 1.0.3)
- [ ] Universal AWS S3 protocol support
- [ ] Aliyun OSS & Tencent COS support
- [ ] Gitee/GitHub image hosting mode
- [ ] Upload history and statistics preview
🛠️ Development
Building from source
npm install # install dependencies
npm run compile # compile TypeScript to ./out/
npm run verify # package a dev VSIX + run smoke tests
npm run package # same as verify but keeps the .vsix on disk
npm run vsce:package # package using the official @vscode/vsce
Commit message conventions
This project follows Conventional Commits so that
standard-version can
auto-generate the changelog and pick the next version number:
| Type |
Triggers version bump |
Section in CHANGELOG |
feat: |
minor |
✨ Features |
fix: |
patch |
🐛 Bug Fixes |
perf: |
patch |
⚡ Performance |
refactor: |
— |
♻️ Refactors |
docs: |
— |
📚 Documentation |
build: |
— |
📦 Build System |
ci: |
— |
🔧 Continuous Integration |
chore: / style: |
— |
hidden |
Breaking changes: append ! after the type, e.g. feat!: rewrite upload pipeline. This
will bump the major version.
Release process
# 1. Make sure conventional commits are in place since the last release
git log v1.0.3..HEAD --oneline
# 2. Run the release script — this bumps package.json, regenerates CHANGELOG.md,
# and creates a single chore(release): X.Y.Z commit. Tag is NOT created yet
# so you can review the diff first.
./scripts/release.sh patch # or minor / major
# 3. Inspect the changes, push the commit, then tag and push the tag
git push
git tag v1.0.4
git push origin v1.0.4
# 4. The GitHub Actions release workflow automatically:
# - packages the VSIX with the official @vscode/vsce
# - smoke-tests it (AWS SDK must require correctly)
# - extracts release notes from CHANGELOG.md
# - attaches the .vsix to a GitHub Release
# - if VSCE_PAT is configured, also publishes to the VS Code Marketplace
A dry run is available without modifying anything:
./scripts/release.sh --dry
Publishing to the VS Code Marketplace
The release workflow calls vsce publish automatically — but only if a VSCE_PAT secret is configured in the repo (Settings → Secrets and variables → Actions). Without it the VSIX is still attached to the GitHub Release, just not pushed to the Marketplace.
To enable automatic Marketplace publishing:
- Sign in to dev.azure.com using the same Microsoft account that owns the
kiang publisher on the Marketplace.
- User settings → Personal Access Tokens → New Token.
- Scopes: Marketplace (Manage) — this is the only scope publish needs.
- Copy the token, then in this repo go to Settings → Secrets and variables → Actions → New repository secret:
- Name:
VSCE_PAT
- Value: the token
- The next tagged release will publish automatically.
If the secret is missing the release workflow logs a warning and the GitHub Release still goes out — so you can also publish manually afterward with:
npx @vscode/vsce publish --packagePath ezimage-1.0.4.vsix
🤝 Contribution & Feedback
If you encounter any issues or have feature suggestions, please:
- Submit an issue on GitHub Issues.
- Join our discussion group for deep conversations.
💬 Feedback
Scan the QR code below to add the author on WeChat, please mention "EzImage":
Scan to get technical support
☕ Support the Author
If EzImage has improved your efficiency, feel free to buy me a coffee! Your support is my greatest motivation for optimization and maintenance.
WeChat Pay
|
Alipay Pay
|
Developed with ❤️ by Kiang