resize-pics
approvedby QuincyLeo
Rescale images to matches the text font size. - This plugin has not been manually reviewed by Obsidian staff.
obsidian-resize-pics
English | 简体中文
Pasting an image into a note or clipping one from a web page often leaves the image scaled larger than the surrounding body text — visually jarring, and tedious to fix one by one. This plugin automatically recognizes the font size of the text baked into each image and rewrites the width digit in the image reference syntax ![[..|W]] /  accordingly.

OCR is powered by Tesseract.js; before first use, cache the language models and other assets from the settings page with one click.
- Minimum Obsidian version: 1.4.0
- Platforms: Desktop only (depends on tesseract.js WASM)
- UI languages: Simplified Chinese / English
- License: MIT
✨ Features
- 📏 Markdown syntax only: image files are left untouched; the plugin only inserts or updates the width digit inside
![[image.png|W]]/ - 🔍 OCR-measured text height: uses tesseract.js (
eng+chi_sim, LSTM engine) to recognize the image, then takes the 20% trimmed mean of line-bbox heights as the pixel height of text baked into the image - 🎯 Match the body font size:
newWidth = round(originalWidth × bodyFontPx / imageTextHeightPx), so text inside images looks visually consistent with the body text in the rendered view - 🛡️ Atomic writes: rewrites go through
Vault.process(read-modify-write); if the note changes during detection, the write is dropped and the user is asked to retry - 📦 On-demand deps: the ~12 MB Tesseract runtime (worker + core WASM + English/Chinese language models) is NOT bundled with the plugin; the user downloads it from the settings page into the plugin folder, with byte-exact SHA-256 verification
- 🈯 Bilingual UI: switch between Chinese/English in settings; command names and button tooltips refresh in place
- 🧹 One-click cleanup: clearing the deps also purges the tesseract.js language-model entries we placed under our IDB namespace (
resize-pics/*insidekeyval-store), so no leftovers remain
🚀 Installation
Manual install (only supported method for now)
<Your Vault>/.obsidian/plugins/resize-pics/
├── main.js
└── manifest.json
- Create the directory
.obsidian/plugins/resize-pics/inside your vault - Run
npm installin the repo root - Edit
TARGET_DIRat the top ofbuild.shto point at the directory above, then run./build.sh— it bundles via esbuild and copiesmain.js/manifest.jsonfor you - Enable the plugin in Obsidian, then open its settings page and click Download dependencies to pull the ~12 MB Tesseract runtime
🖱️ Triggering
Open a Markdown document (Reading view / Live Preview / Source mode all work), then choose one of:
- Click the Ribbon icon on the left
- Command palette →
Resize pics to font size/将图片缩放到与正文字号一致
The result is reported via a Notice:
resize-pics: resized N image(s) (skipped M).
Or, when there are no images to work on:
resize-pics: no resizable images found in the current view.
⚙️ Settings
Path: Settings → Community plugins → resize-pics
Language / 语言
| Value | Description |
|---|---|
zh-CN (default) | Chinese UI, command name "将图片缩放到与正文字号一致" |
en | English UI, command name "Resize pics to font size" |
Switching updates the command palette entry and Ribbon tooltip live (internally re-registers via removeCommand + addCommand).
OCR dependencies
The settings page shows the status of 4 required assets (ready / missing) plus two buttons:
| Button | Behaviour |
|---|---|
| Download dependencies | Fetches the 4 files below from the CDN into <plugin>/tesseract-data/, verifying each against its pinned SHA-256; interrupted, failed, or hash-mismatched files are retried on the next click |
| Clear cache | Deletes the 4 files under tesseract-data/, removes that folder if it ends up empty, and evicts every resize-pics/-prefixed key from the IndexedDB keyval-store |
Dependency manifest (current version):
| File | Source | Notes |
|---|---|---|
worker.min.js | jsDelivr / tesseract.js@5.1.1 | tesseract.js main-thread scheduler script |
tesseract-core-simd.wasm.js | jsDelivr / tesseract.js-core@5.1.1 | SIMD core with WASM inlined |
eng.traineddata.gz | tessdata.projectnaptha.com/4.0.0_fast | English language model |
chi_sim.traineddata.gz | Same | Simplified Chinese language model |
Versions and hashes are consolidated in the REQUIRED_ASSETS array at the top of src/tesseractDeps.js.
🏗️ Workflow
┌──────────────────────────────────────────────────────────────┐
│ User clicks Ribbon / triggers command │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ main.js · ResizePicsPlugin.resizePicsToFontSize() │
│ · Check: not concurrent / no deps op running / MarkdownView │
│ has a file │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ src/resize.js · ResizeImagesJob.run() │
│ · Read body font size bodyFontPx (getComputedStyle) │
│ · vault.read() the current note content │
│ · metadataCache.getFileCache() → embeds + sections │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ src/getPics.js · PicSource │
│ · collectReferences(embeds, sections): │
│ local (embeds) + external (regex on safe sections) │
│ merged, sorted DESC by offset, non-overlapping │
│ · resolve(edit) → {kind, tfile|bytes, dims, ...} │
│ - local: getResourcePath + loadImageDimensions │
│ - external: requestUrl retry 3× + blob-URL dims │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ ResizeImagesJob.rewriteContent() — resize concerns only │
│ · For each edit, dispatch on picSource.resolve().kind │
│ · imageTextHeightDetector.ensureWorker() lazily on first use│
│ · detect({bytes | resourcePath}) → line heights → trimmed │
│ mean → imageTextHeightPx │
│ · newWidth = round(naturalWidth × bodyFontPx / textHeightPx)│
│ · Rewrite the size segment for both wikilink & standard MD │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ vault.process(file, current => { │
│ if (current !== original) conflict → abort │
│ else return newContent │
│ }) │
└────────────────────────────┬─────────────────────────────────┘
│
▼
✅ Notice: resized N image(s) (skipped M)
🧱 Code layout
resize-pics/
├── manifest.json # Plugin metadata
├── main.js # esbuild output; the file Obsidian loads
├── package.json # Build / check / test scripts
├── build.sh # One-shot build + copy to a target vault plugin dir
│
├── src/
│ ├── main.js # Plugin entry, command/Ribbon registration
│ │ # · ResizePicsPlugin (lifecycle + concurrency gate)
│ │
│ ├── settings.js # Settings store + settings-panel UI
│ │ # · LANGUAGE_OPTIONS (bilingual copy)
│ │ # · SETTINGS_SCHEMA_VERSION (=1; unknown versions refused)
│ │ # · ResizePicsSettings (load/save with snapshot concurrency)
│ │ # · ResizePicsSettingTab (UI + status refresh + download/clear)
│ │ # · bilingualError (used before language is known)
│ │ # · localizedError (used after language is loaded)
│ │
│ ├── getPics.js # Image discovery + fetching (no OCR / no rewrite)
│ │ # · IMAGE_EXT_RE / OCR_SUPPORTED_EXT_RE
│ │ # · EXTERNAL_URL_RE / EXTERNAL_HTTPS_RE
│ │ # · SAFE_SECTION_TYPES = {paragraph, list, blockquote, callout}
│ │ # · EXTERNAL_FETCH_MAX_ATTEMPTS=3, BACKOFF_MS=200
│ │ # · collectImageReferences (embeds → edit list)
│ │ # · collectExternalImageReferences (sections → edit list)
│ │ # · fetchExternalImageBytes (requestUrl + linear backoff)
│ │ # · loadImageDimensions[FromBytes] (URL or blob-URL path)
│ │ # · PicSource (collectReferences + resolve)
│ │
│ ├── resize.js # Resize pipeline + Markdown-syntax rewriting
│ │ # · CONTENT_ROOT_SELECTORS (reading/edit-view font source)
│ │ # · MAX_SCALE_RATIO=10 (symmetric clamp)
│ │ # · rewriteWikilinkInner / rewriteStandardAlt
│ │ # · ResizeImagesJob (run / rewriteContent / dispose)
│ │
│ ├── ocr.js # tesseract.js integration
│ │ # · CACHE_PATH="resize-pics" (IDB key namespace)
│ │ # · MIN_LINE_CONFIDENCE=60, MIN_LINE_HEIGHT_PX=4
│ │ # · MIN_LINES_REQUIRED=2, TRIM_FRACTION=0.2
│ │ # · ImageTextHeightDetector (detect({bytes} | {resourcePath}))
│ │ # · idbPutAll / evictOcrCache (seed & clear IDB)
│ │
│ └── tesseractDeps.js # Dependency manifest + download / verify / clear
│ # · REQUIRED_ASSETS (name + url + sha256)
│ # · TesseractDependencyManager
│ # - checkStatus({verifyHash})
│ # - downloadAll(onProgress) (cases A/B/C)
│ # - clearCache() (incl. IDB cleanup)
│
├── ut/
│ ├── main.test.js # Plugin-level: command registration, Notice copy, concurrency gate
│ ├── getPics.test.js # Image discovery + fetching: PicSource.collectReferences / resolve
│ ├── resize.test.js # Editor-level: Markdown-syntax rewrite rules + OCR accounting
│ ├── settings.test.js # Settings page: status refresh, button state, download/clear callbacks
│ ├── tesseractDeps.test.js # Deps mgmt: cases A/B/C, cancel, IDB cleanup after seed
│ ├── concurrent.test.js # Concurrency / lifecycle: async settles across unload
│ └── helpers/ # Test infrastructure
│ ├── bootstrap.js # · Fake obsidian module + global DOM/IDB stubs (incl. requestUrl)
│ ├── fakePlugin.js # · Substitutable plugin/app/adapter/vault fakes
│ └── loadTesseractDeps.js
│
├── LICENSE # MIT
└── README.md / README.zh-CN.md
🔐 Security design
- End-to-end verified deps: every Tesseract runtime file has its SHA-256 recomputed and compared against the constants in
REQUIRED_ASSETSboth before being written to disk and on everycheckStatus()call; a mismatch → the file is removed from disk and reported as failed, and only the user's next "Download" click will retry. Cross-version or supply-chain-poisoned old bytes are never used silently - Namespaced IDB entries: tesseract.js writes traineddata into the shared
keyval-store/keyvalby default; every key this plugin writes carries aresize-pics/prefix, and cleanup walks the store with a cursor +startsWithfilter, so other plugins' entries are never touched - Atomic note writes: uses the
Vault.process(file, updater)atomic-write API and lets Obsidian's core do conflict detection; whencurrent !== original, the write is dropped and the user is notified "do not edit the file during detection, please retry" cacheMethod: "readOnly": on init failure, tesseract's own recovery tries todel()the traineddata IDB key it was pointed at; read-only mode disables that side effect and preserves the bytes we pre-populated- Working around the same-origin rule: Obsidian's main renderer runs at
app://obsidian.md, andnew Worker("app://<vault-uuid>/…")is rejected cross-origin. The plugin therefore reads the 4 binaries viaadapter.readBinaryinto main-thread memory and wraps the worker + core withURL.createObjectURL(blob URLs inherit their creator's origin), bypassing the boundary entirely - Unload defenses:
onunloadsynchronously flips__resizePicsUnloadedand bumps_lifecycleGeneration; every async continuation re-checks both at eachawaitpoint, so a late resolution can never emit a new Notice, mutate a button state, or write a file on disk
⚠️ Known limitations
- Desktop only (
isDesktopOnly: true): the tesseract.js worker + WASM depends on desktop Electron - OCR-supported formats:
png,jpg/jpeg,webp,bmp. Formats Obsidian recognizes as images but tesseract can't decode (gif,svg,avif,tiff) are counted as "considered but skipped" - External images are downloaded, verified in
cache.sections, retried up to 3× per image:doesn't appear in Obsidian'smetadataCache.embeds, so the plugin scans body-levelcache.sections(paragraph,list,blockquote,callout) for the syntax, then downloads viarequestUrl(Obsidian's CORS-immune HTTP client) and runs the same OCR + size-formula path as local images. Non-http(s)URIs (file://,app://,data:) are still skipped - Scale clamp: the
bodyFontPx / imageTextHeightPxratio is clamped to[1/10, 10]; images outside that range are treated as "extreme text-height outliers" (usually a 1-px noise pixel misrecognized as text) and skipped - ≥ 2 usable lines required: a single line is too small a sample and can be pulled around by a misrecognized line; images with fewer usable lines are skipped
- Minimum line confidence = 60: any line below this confidence is dropped; dashed borders, JPEG blocking artefacts, and single-pixel dust are frequently seen as 1-px-tall "text" by tesseract
- Dependency versions are hard-pinned: the URL + SHA-256 in
REQUIRED_ASSETSare completely fixed; upgrading tesseract.js requires refreshing both sets of values
🐛 Common notices / error messages
| Notice / Exception | Explanation / Handling |
|---|---|
resize-pics: open a Markdown view first. | No active Markdown view, or the view has no file bound |
resize-pics: no resizable images found in the current view. | No OCR-capable local images in the view (external links, non-image extensions, cache not built, …) |
resize-pics: Do not edit file during detection. Please try again. | Note content changed while OCR was running; Vault.process detected the conflict and dropped the write |
resize-pics: OCR dependencies not ready (X/Y missing: …); please download them in Settings. | Deps missing at OCR time — go to the settings page and click "Download dependencies" |
resize-pics: failed to read body font size. | Neither .markdown-preview-view nor .cm-content was found in the view container |
resize-pics: body font size is invalid. | Computed font-size is not a positive finite number (unusual theme?) |
resize-pics: this Obsidian version does not support atomic file updates (Vault.process); please upgrade Obsidian. | Obsidian too old, missing Vault.process |
resize-pics: this Obsidian cannot read plugin-folder assets. | The current adapter has no readBinary (mobile / unusual environment) |
resize-pics: unsupported settings version X; expected 1. | Old data.json + new plugin; wipe data.json or downgrade |
resize-pics: dependency download partially failed: X/Y (…). | See the structured resize-pics: dependency download failures log in DevTools |
🛠️ Development notes
- Bundled build: Obsidian only loads the root
main.jsfrom a plugin folder — other JS filesrequired from it are not synced by Obsidian Sync. Source therefore lives undersrc/, andnpm run build(esbuild) bundles the whole dependency graph into the rootmain.js - One-shot build + distribute: after editing
TARGET_DIRat the top ofbuild.sh, running it doesnpm run buildand copiesmain.js/manifest.jsoninto the target vault's plugin directory - Tests:
npm testuses Node's built-innode --test; no external framework - package.json scripts:
npm run build— esbuild bundlenpm run check— build +node --check main.js(syntax self-check)npm test— runut/*.test.js
- Debug logs: every
console.log/erroris prefixed withresize-pics:; filter for it in Obsidian's DevTools
Key constants
| Constant | Value | Location |
|---|---|---|
PLUGIN_VERSION | 0.2.0 | src/main.js |
RESIZE_COMMAND_ID | "resize-pics-to-font-size" | src/main.js |
RIBBON_ICON_ID | "image-upscale" | src/main.js |
NOTICE_DURATION_MS | 4000 | src/main.js, src/settings.js |
SETTINGS_SCHEMA_VERSION | 1 | src/settings.js |
MAX_SCALE_RATIO | 10 | src/resize.js |
EXTERNAL_FETCH_MAX_ATTEMPTS | 3 | src/getPics.js |
EXTERNAL_FETCH_BACKOFF_MS | 200 | src/getPics.js |
MIN_LINE_CONFIDENCE | 60 | src/ocr.js |
MIN_LINE_HEIGHT_PX | 4 | src/ocr.js |
MIN_LINES_REQUIRED | 2 | src/ocr.js |
TRIM_FRACTION | 0.2 | src/ocr.js |
OCR_LANGS | "eng+chi_sim" | src/ocr.js |
CACHE_PATH | "resize-pics" | src/ocr.js |
DATA_SUBDIR | "tesseract-data" | src/tesseractDeps.js |
TESSERACT_JS_VERSION | "5.1.1" | src/tesseractDeps.js |
TESSERACT_CORE_VERSION | "5.1.1" | src/tesseractDeps.js |
📄 License
MIT © 2026 QuincyLeo (Quincy-Leo)
See LICENSE for details.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.