9.5 KiB
Reading mode embed rendering
Why it exists
Ink embeds in Live Preview are rendered by CodeMirror 6 widgets that mount React components with custom layout, sizing, and preview chrome. Reading mode uses Obsidian’s native markdown renderer, which outputs a bare internal-embed (or img) plus a separate Edit link. Without a dedicated reading-mode path, plugin CSS and dimension logic never run, so embeds look wrong when switching from edit to read view.
Option A adds a markdown post-processor that replaces native ink embeds with the same preview components used in Live Preview (read-only).
Conceptual understanding
flowchart LR
MD["Note markdown"]
ObsRenderer["Obsidian reading renderer"]
NativeEmbed["internal-embed + Edit link"]
PostProc["Ink MarkdownPostProcessor"]
RenderChild["InkReadingEmbedHost"]
Preview["DrawingEmbedPreview / WritingEmbedPreview"]
MD --> ObsRenderer --> NativeEmbed
NativeEmbed --> PostProc
PostProc -->|"parse Edit URL, resolve TFile"| RenderChild
RenderChild --> Preview
Sizing and framing settings live in the Edit link URL (width, aspectRatio, viewBox*), not on the image markdown line. The post-processor reads those params and applies the same layout rules as Live Preview preview mode.
Why Option A (implemented) vs Option B (not implemented)
Option B — how it would work
On each resize/save, also write dimensions into the image markdown using Obsidian’s pipe syntax, for example:
 [Edit Drawing](...)
Reading mode’s native image renderer would size the embed from the pipe dimensions without custom plugin rendering.
Why we chose Option A instead
| Concern | Option B | Option A |
|---|---|---|
| Drawing viewBox / reframing | Shows the entire SVG; cannot apply the framed viewBox from the Edit link |
DrawingEmbedPreview applies viewBox so the user sees the crop they configured |
| Full canvas vs framed size | Saved width × aspectRatio describe the framed viewport, not the full canvas. If the embed showed all strokes on a large canvas, a fixed box sized for the current frame would be too small — the embed should use as much of the content column as possible. Option B cannot adapt to that. |
Framed preview matches what the user set; future full-canvas display can be handled in the preview pipeline |
| Preview chrome | No frames, backgrounds, or writing lines from plugin settings | Reuses preview SCSS and settings |
| Full-bleed (drawings) | Not available | applyReadingModeAncestorStyling negates preview padding |
| Writing embeds | Writing already fills 100% content width; height follows aspect ratio — Option B adds little | Same fluid layout as Live Preview |
| Source of truth | Two places to keep in sync (image pipe + Edit URL) on every resize | Edit URL remains canonical; one rendering path |
Future consideration: Option B could be added later as an optional fallback (e.g. a plugin setting) for environments where post-processing fails. It is not implemented today and is not a substitute for framed drawing previews.
Flows
- User opens a note in Reading mode.
- Obsidian renders
 [Edit Drawing](<url>?type=inkDrawing&width=…&aspectRatio=…&viewBox…=…). registerReadingModeInkEmbedspost-processor runs on each preview section (or on the full.markdown-preview-viewduring PDF export — see PDF export).findReadingModeInkEmbedCandidateslocatesinternal-embed/img+ sibling Edit link, parses settings, resolvesTFileviacontext.sourcePath.- Native block is replaced with
InkReadingEmbedHost(MarkdownRenderChild). - Host mounts
DrawingEmbedPrevieworWritingEmbedPreviewinside a sized.ddc_ink_resize-container. - Drawing embeds call
applyReadingModeAncestorStylingfor full-bleed; writing embeds receive the embed-block class only (same as Live Preview).
Technical details
| Piece | Location |
|---|---|
| Post-processor registration | src/components/formats/current/reading-mode/register-reading-mode-ink-embeds.ts |
| Embed detection | src/logic/utils/detect-reading-mode-ink-embed.ts |
| React host | src/components/formats/current/reading-mode/ink-reading-embed-host.tsx |
| Reading-mode full-bleed | applyReadingModeAncestorStyling() in src/logic/utils/embed.ts |
| Settings parser (reused) | src/components/formats/current/utils/parse-settings-from-url.ts |
Dimension rules (match Live Preview preview mode):
- Drawing:
widthfrom settings (capped to preview container width),height = width / aspectRatio, centered in the resize container. - Writing:
width: 100%,height = containerWidth / aspectRatio.
Embeds are non-interactive in reading mode (no click-to-edit).
PDF export
Built-in Export to PDF uses the same reading-mode HTML pipeline, not Live Preview widgets. Obsidian renders the note into a temporary print DOM (under .print), then converts it with Electron’s printToPDF.
flowchart LR
Note["Note markdown"]
PrintDOM["Temporary .print preview DOM"]
PostProc["Ink MarkdownPostProcessor"]
Preview["DrawingEmbedPreview / WritingEmbedPreview"]
PDF["Exported PDF"]
Note --> PrintDOM --> PostProc --> Preview --> PDF
Why the post-processor must handle PDF differently
In Reading mode, registerMarkdownPostProcessor receives section-level elements — p, .el-p, blockquote wrappers, and similar. Ink scans each block for an embed marker plus its Edit link.
In PDF export, Obsidian passes the entire page as a single element — typically .markdown-preview-view.markdown-rendered — not individual sections. This matches behaviour reported by other plugin authors (Obsidian forum: post-processors and PDF export).
If Ink only accepted section roots, the post-processor would return immediately during export. Embeds would stay as native internal-embed / img nodes, which:
- Show the full SVG canvas (no
viewBoxcrop from the Edit link) - Ignore saved width and aspect ratio (often filling the page)
Drawing reframing is stored in the Edit link URL (viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight, plus width and aspectRatio). Only DrawingEmbedPreview applies that crop after inlining the SVG — native image embeds cannot.
Lifecycle constraints on full-page export
On the full-page PDF path, Ink mounts fresh InkReadingEmbedHost instances for every embed in one pass. Two rules prevent export from hanging:
-
Skip stale-host remount on full-page roots —
remountStaleReadingEmbedHostsInRootexists to recover cached reading-view DOM when toggling Live Preview ↔ Reading mode. On PDF export, all hosts were just created; running remount synchronously races React commit (host isACTIVEbut.ddc_ink_embedis not in the DOM yet). The remount path usesplugin.addChildinstead ofcontext.addChild; those orphans never unload when the print DOM is discarded, which can leave Obsidian’s “Exporting to PDF” progress bar stuck even after the file is written. -
Defer stale remount elsewhere — For section-level reading mode, stale recovery runs in
requestAnimationFrameso React can commit before the “missing embed” check runs.
Implementation: FULL_PAGE_PREVIEW_ROOT_SELECTOR and the deferred remount block in register-reading-mode-ink-embeds.ts.
Obsidian print CSS (optional for Ink)
PDF export often requires rules under @media print { .print … }. Frontmatter cssclass values may not appear on the print DOM. Ink does not ship print-specific CSS today; embed layout comes from the same preview components and inline dimensions as Reading mode. If custom export styling is added later, test against a real PDF export, not just Reading mode.
Technical gotchas
- Obsidian may render a
span.internal-embedrather thanimg— the detector handles both. - A previous reverted processor targeted
imgonly and used a bare<img>without preview components orviewBox. - The Edit link must be removed from the DOM when replacing the block, not only hidden with CSS.
- Transclusion requires
context.sourcePathwhen resolving embed file paths, not the active editor file. - Blockquote right-edge overflow is a known Live Preview limitation; reading mode follows the same behaviour.
- PDF export uses a full-page post-processor root — See PDF export. Do not remove
FULL_PAGE_PREVIEW_ROOT_SELECTORor revert to section-only scan roots without testing reframed drawing exports. - Do not run stale-host remount synchronously after mounting on full-page roots — Causes a React race and can hang the export progress bar; see PDF export § Lifecycle constraints.
- Drawing embed centering is CSS, not inline literals —
.ddc_ink_drawing-embed .ddc_ink_resize-containerownsposition/left: 50%/translate: -50%.applyReadingModeEmbedDimensionsonly sets dynamic width/height/maxWidth (Obsidianno-static-styles-assignment). Writing width100%comes from writing-embed SCSS.
See also
- Reading mode — Conceptual overview (non-technical)
- Ink colours and theming — How preview colours follow the theme
- Ink embeds: contexts and limitations
- UX decisions