Skip to content

Uneven spacing and clipped glyphs in fractionally scaled headings #65

Description

@denny0223

In the setup below, fractionally scaled headings show large gaps between Han-character runs and clipped Latin glyphs, even with a stationary cursor. The sample uses Han characters shared by Chinese and Japanese; neither repeated-character heading contains spaces.

MdRender preview: gaps between Han-character groups and clipped M glyphs

At full size, compare the right edge of the 8th, 16th, and 24th M with its neighbors.

Reproduction

Tested on unmodified 00a83b8a25b9d2d50dd15efaf4c701c19de21688: Kitty 0.47.1, Neovim 0.12.5, Fedora 46 prerelease; direct Kitty/X11 without tmux. At 12 pt, cells were 19 × 44 px. Font logs confirmed Noto Sans Mono with Noto Sans Mono CJK TC fallback. A direct OSC 66 rerun on Kitty 0.48.2 reproduced the 12/10 pt measurements below. Current upstream HEAD, later Kitty releases, and other operating systems remain untested.

Save as /tmp/md-render-repro.lua:

-- Start Neovim from the md-render.nvim checkout being tested.
vim.opt.runtimepath:prepend(vim.fn.getcwd())
vim.opt.termguicolors = true
vim.opt.cursorline = false

Save as /tmp/heading-spacing.md:

## 漢字表示間隔確認

Body after the shared Han-character heading.

## 田田田田田田田田田田田田

Body after the repeated Han characters.

## MMMMMMMMMMMMMMMMMMMMMMMM

Body after the English heading.

From the plugin checkout, open Kitty with:

env -u TMUX -u TMUX_PANE -u TERM_PROGRAM -u TERM_PROGRAM_VERSION \
  kitty --config NONE --debug-font-fallback \
  -o remember_window_size=no -o initial_window_width=80c \
  -o initial_window_height=16c -o font_size=12 \
  -o linux_display_server=x11 --directory "$PWD" \
  nvim --clean -n -u /tmp/md-render-repro.lua /tmp/heading-spacing.md \
  2>/tmp/md-render-kitty-fonts.log

Run :MdRender toggle. --clean excludes personal plugins; disabling cursorline isolates this from #64. --config NONE does not fix fallback fonts across machines; the debug log records the fonts actually selected.

Experiments

The same h2 runs reproduce the artifacts directly in Kitty, without Neovim. Disabling scaling removes them in the same document.

Direct reproduction, measurements, and unscaled control

Run this Python script in Kitty; no third-party packages are needed. Each run contains four 田 or eight M:

import sys

for label, chunk in (("Han characters", "田" * 4), ("ASCII", "M" * 8)):
    sys.stdout.write(label + "\r\n\x1b[1m")
    for _ in range(3):
        sys.stdout.write(f"\x1b]66;s=2:n=7:d=8:w=7:v=2;{chunk}\x1b\\")
    sys.stdout.write("\x1b[0m\r\n\r\n\r\n")
sys.stdout.flush()

All rows below use h2 scaling. Backgrounds mark run boundaries; centered adds h=2, and per-character uses w=0.

Direct OSC 66: original, centered, and per-character placement

Screenshot measurement 12 pt 10 pt
田 left-edge advance, within/across runs 56 / 98 px 47 / 83 px
田 blank gap, within/across runs 10 / 52 px 8 / 44 px
M visible ink width, ordinary/run-end 29 / 26 px 24 / 24 px

These are visible-pixel bounds, not font-internal metrics. At 10 pt, all 24 M glyphs have equal ink widths and advances, while the Han-character gaps remain.

Tracing Kitty 0.48.2's hb_shape() output confirmed that, at 12 pt, a 266 px run contains 224 px of Han advances (4 × 56) but 272 px of Latin advances (8 × 34). Changing only the M advance from 34 to 33 px restored all ink widths to 29 px, with Han measurements unchanged. This was a diagnostic intervention, not a proposed fix.

For the unscaled control, add require("md-render.text_size").setup { enabled = false } to the Lua config and restart:

Same document with scaling disabled

Experiment / idea Result / limitation
Center runs (h=2) At 12 and 10 pt, shifts each run equally without changing spacing or clipping; the 12 pt Han shift is 21 px.
Per-character placement (w=0) Kept all six scales and corrected wrapping to use reserved cells (s=2), then checked 80/40-column previews. Boundary artifacts disappeared in the samples, but spacing became too wide. At 80 columns, a one-line mixed Chinese/English h3 wrapped to two. Rejected for readability.
Reduce h2–h6 widths to max(1, w - 1) Kept text, run boundaries, scales, and fonts unchanged; h1 was the control. Noto glyphs joined/overlapped at h2/h3/h5 and clipped at h4. Droid Sans Japanese retained all glyphs and ink widths, but gaps remained.
Integer sizes / scaling off An exploratory preview used h2 at 2× and h3–h6 at ordinary size. This and disabling scaling lose the six-size hierarchy.
Standalone image reference Standalone references using Pango (text layout library) and Cairo (2D graphics library) preserved the six scales, with no missing glyphs or horizontal overflow in the 80/40-column samples. No plugin integration; font matching, redraw, and interactions remain untested.
Glyph-aware native packing (untested) Use actual terminal glyph layout to guide splitting/allocation; no portable metric source or validated algorithm yet.
Experiment images and readability comparison

The original scales are 2, 7/4, 3/2, 7/5, 5/4, 7/6. The Han-character diagnostic rows below each contain twelve 田.

Original Noto packing, followed by per-character placement:

Original native packing, h1–h6

Per-character placement, h1–h6

Level Ink width Advance Blank gap
h1, unchanged 54 px 76 px 22 px
h2 46 px 76 px 30 px
h3 40 px 76 px 36 px
h4 37 px 76 px 39 px
h5 34 px 76 px 42 px
h6 31 px 76 px 45 px

At h4–h6, blank gaps exceed glyph widths. Uniform spacing alone does not ensure readability.

Reduced widths with Noto; joined ink regions do not mean input characters were deleted:

Noto after reducing w: overlap and clipping

Droid Sans Japanese (confirmed in Kitty's font log), before/after reducing widths. Its cells are 32 × 42 px versus Noto's 19 × 44 px; compare within each font, not absolute distances across fonts.

Droid Sans Japanese, original widths

Droid Sans Japanese, reduced widths

Integer-scale preview and Pango/Cairo reference, using an earlier Chinese/English fixture:

Integer preview: h2 at 2x, h3–h6 at ordinary size

Pango/Cairo reference at six scales

The Pango image is a standalone typography reference, not an MdRender fix or Kitty rendering. It uses the same Noto font families; keeping source text alone would not establish working image-based interactions.

For the 12 pt sample, Han fallback advances leave unused space, while hinted Latin advances exceed the allocated width. The OSC 66 specification separates font scale from allocation and allows truncation of text that does not fit. A general native packing rule still needs validation across fonts and sizes. These tests do not cover all Unicode wrapping or Neovim redraw/interaction behavior.

Discussion

I hope these examples and experiments are useful. My interest is in improving readability while keeping the six heading sizes and familiar text interactions: search/highlights, selection/copying in Neovim and the terminal, and links. I'm also open to image rendering if it can preserve the original Neovim text and those interactions.

I'm curious whether you've seen similar behavior, and would welcome any thoughts or context you'd like to share.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions