asciinema / asciinema-player

Web player for terminal session recordings
Apache License 2.0
2.65k stars 261 forks source link

Wrong spacing in thumbnail/embed SVG with Consolas font #265

Open lionel-rowe opened 1 month ago

lionel-rowe commented 1 month ago

Edit: I realized this issue is in the wrong repo, feel free to move to asciinema/asciinema-server. PR at https://github.com/asciinema/asciinema-server/pull/450


Describe the bug

With this recording: https://asciinema.org/a/XxTbS0aAtTA6BZ9tdzKardxJs

The recording itself (including the freeze frame of the frame used for the thumbnail) looks perfect, stunning, gorgeous.

However, the spacing is off in the SVG used for the thumbnail/embed image when displayed on Windows 11 with the Consolas font installed (from examining the SVG, the font stack is "Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace, 'Powerline Symbols'").

Changing the font to Courier New fixes the problem (but it also makes it kinda ugly cuz... Courier New). Changing the font size to 15.3px (magic number) also seems to fix it, but presumably would break the other fonts.

Screenshots:

Description Screenshot
Freeze frame of the recording itself Screenshot of a freeze frame of the recording itself
SVG with Consolas font installed (broken spacing) Screenshot of SVG with Consolas font installed
SVG with the font manually changed to Courier New (fixed but ugly) Screenshot of SVG with font changed to Courier New
SVG with the font size manually changed to 15.3px (fixed but magic number) Screenshot of SVG with font changed to Courier New

Direct link to the SVG

To Reproduce Steps to reproduce the behavior:

  1. Create a recording containing various ANSI code spans of different colors/font weight/etc
  2. View the thumbnail with the Consolas font installed

There are prominent examples at https://asciinema.org/explore that also reproduce the problem, e.g. https://asciinema.org/a/666780

Expected behavior

Thumbnail to look identical (or near-identical) to screenshotted freeze frame, just crisper due to being an SVG.

Versions:

Additional context N/A

lionel-rowe commented 1 month ago

Here's a hack that "fixes" such an SVG in any font (heck, it even works passably for most variable-width fonts), by means of wrapping each char in its own span:

  1. Open SVG in browser
  2. Run script in dev tools
  3. Copy XML of the SVG element (using outerHTML) and save it in an .svg file

Script:

for (const tspan of document.querySelectorAll('tspan')) {
    if (tspan.querySelector('*')) continue

    const tspans = [...tspan.textContent.trim()].map((char, i) => {
        const t = tspan.cloneNode(true)
        t.textContent = char

        let x = parseFloat(tspan.getAttribute('x'))
        x += i * 8.4

        if (i) t.removeAttribute('dy')
        t.setAttribute('x', x)

        return t
    })
    tspan.replaceWith(...tspans)
}

Limitations:

Results applied to my thumbnail:

image

And here are the results with the font changed to the decidedly non-monospace Papyrus:

image

lionel-rowe commented 1 month ago

Less hacky fix: add a @font-face at-rule targeting Consolas inside the <style> tag:

@font-face {
    font-family: "Consolas";
    src: local("Consolas");
    size-adjust: 109.5%;
}

As before, 109.5 is a magic number (109.5% of 14px ~= 15.3px).

Works perfectly in latest Chrome, Firefox, Edge, but notably fails (has no effect) if you open the SVG in Inkscape.

Result:

Font size-adjusted version

lionel-rowe commented 4 weeks ago

OK, here's a third fix, much better and less hacky than either of the others.

The x property of <tspan>s can accept a space-separated list of numbers, rather than just a single scalar number, with each number corresponding to one glyph. This feature is supported even in SVG 1.1, so it has very wide support.

In addition, CSS font-size-adjust is now "newly available" in baseline, meaning it has decent (but not completely ubiquitous) browser support.

Tested in latest Chrome, FF, Edge, and it even looks fine in Inkscape (despite no font-size-adjust support). I found the second solution above suffers from a rendering bug in FF wherein zooming in and out sometimes messes up the layout, but this solution doesn't have that problem.

Also tested on an asciicast with different Unicode-width chars — no issues there either, as asciinema already renders each char with Unicode-width other than 1 in its own span, so they just stay where they are.

File size increase in my case is 10.2 -> 13.1 kB, though I cheated a little by rounding each x value to 1 decimal place (no visual difference).

Dev-tools-based demo:

document.querySelector('svg style').textContent += `\n text { font-size-adjust: ch-width 0.6; }`

// https://github.com/asciinema/asciinema-server/blob/fbbb2ce35272d516ce2a6debfbc04c9a7c60a149/lib/asciinema_web/controllers/recording_svg.ex#L399
const CELL_WIDTH = 8.42333333

for (const tspan of document.querySelectorAll('tspan')) {
    if (tspan.childElementCount) continue

    const offset = parseFloat(tspan.getAttribute('x'))

    const xPerGrapheme = [...new Intl.Segmenter('en-US', { granularity: 'grapheme' }).segment(tspan.textContent.trim())].map((_, i) => {
        return offset + (i * CELL_WIDTH)
    })

    tspan.setAttribute('x', xPerGrapheme.map((x) => x.toFixed(1)).join(' '))
}
lionel-rowe commented 2 weeks ago

In addition, CSS font-size-adjust is now "newly available" in baseline, meaning it has decent (but not completely ubiquitous) browser support.

Turns out it breaks in Chrome when a width or max-width is applied to the img element in which the SVG is rendered (issue) So unless there's a workaround, the least-worst option is probably the same fix but without the font-size-adjust bit.