ansi2html icon indicating copy to clipboard operation
ansi2html copied to clipboard

Unreadable colors with light backgrounds

Open rayosborn opened this issue 6 months ago • 5 comments

This has been a valuable and widely used package for many years, although I gather that it is minimally maintained even after requests for additional support. My issue is related to #225, but I am not requesting changes to the package itself, but tips from the user community about how to make it usable, in particular with a light background.

I am using ansi2html in a Python package that outputs the colored text to a PyQt QTextEdit widget, or in the examples I provide here, a QMessageBox. I use the following code:

def convertHTML(text, dark_bg=None):
    """Replace ANSI color codes with HTML"""
    if dark_bg is None:
        dark_bg = in_dark_mode()
    conv = Ansi2HTMLConverter(dark_bg=dark_bg, inline=True)
    return conv.convert(text)

In dark mode, this works fine: Image However, in light mode, the text is nearly unreadable. Image Although there is evidence of ansi2html switching some white text to black, other off-white text used for much of the code is unreadable and the whole color palette is washed out. As far as I can tell, none of the supported color schemes improve the situation.

So my question is how people get around this. Surely I am not the only one who has confronted this issue. I have seen comments about changing a gray background to white, using text.replace(), but that makes things worse. What is needed is a revised default color palette that works for light backgrounds. Do they exist?

rayosborn avatar Jul 11 '25 17:07 rayosborn

I guess the answer to my own question is that this is considered to be outside the scope of ansi2html, i.e., the mode converts whatever ANSI escape codes are used. The problem is that so many packages generate codes that are suitable for a black background, so I think it would still be useful to have a feature that inverts the default colors for a light background, but there may be no obvious algorithm for doing that.

I briefly closed this issue, but I have opened it again because, although it could legitimately be considered to be outside the scope of this package, it is nevertheless a real issue that I would welcome comments on. The reason is that, although the colors may have been initially selected for light or dark mode, it may be necessary to view the text in the opposite mode. So the question is whether there are any ways of inverting the color scheme, perhaps provided by another package, to cope with this scenario.

rayosborn avatar Jul 11 '25 18:07 rayosborn

I wrestled with the same issue when writing the code to create an HTML report in pytest-tui. I ended up providing the user with three choices as workarounds: 1) invert all colors, 2) make the background black, 3) remove all coloring altogether. This was all done through JavaScript functions and CSS style definitions. None of these was ideal, but given I had a user base of exactly one (me), I didn’t sink anymore effort into it.

IIRC, ansi2html ends up defining a huge CSS section in the HTML file that has all the color definitions in it, but that is only for black background, hence my above approach. I guess you could “invert” all those colors somehow, but I do not know how to do that within the constraints of the ansi2html code without digging in and studying it. Maybe the way to do it these days would be to ask ChatGPT how to make those colors pop on a light background in your application. Good prompt engineering would explain exactly what it is you’re trying to do, and with what resources, and it’ll likely give you a pretty good way to approach it.

Let me know what you find out. I’m kinda curious. Also let me know if you want a sample of the output HTML file from my repo for you to see how the inversion code works.

jeffwright13 avatar Jul 12 '25 00:07 jeffwright13

@jeffwright13, thank you for your response. I liked your idea of giving the option to switch color schemes, so I have now implemented it in NeXpy. If the user encounters unreadable text, they can dynamically switch to the opposite mode. In my case, I have log files that might have been updated in light or dark mode, so there is no way of knowing in advance whether I need to invert colors. Dynamic switching saves me the need to bother ChatGPT for now, anyway.

rayosborn avatar Jul 12 '25 15:07 rayosborn

I just convert all colors to HLS, and then ensure that all foreground colors are between 0% and 40% light, and all background colors are between 60% and 100% light.

import re
import sys
import colorsys


def clamp(x, _min, _max):
    return max(_min, min(x, _max))


def hsl_clamp_l(hex: str, _min: float, _max: float):
    assert hex.startswith("#") and (len(hex) == 7)
    r, g, b = int(hex[1:3], 16), int(hex[3:5], 16), int(hex[5:7], 16)
    h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
    l = clamp(l, _min, _max)
    r_new, g_new, b_new = [int(x * 255) for x in colorsys.hls_to_rgb(h, l, s)]
    return f"#{r_new:02x}{g_new:02x}{b_new:02x}"


COLOR_RE = re.compile(r"([a-z]*-?color:#[0-9a-f]{6})")  # color:#ffffff or background-color:#ffffff
for chunk in re.split(COLOR_RE, sys.stdin.read()):
    if re.match(COLOR_RE, chunk) and chunk.startswith("color:"):
        hex = chunk[len("color:") :]
        new_hex = hsl_clamp_l(hex, 0, 0.4)  # foreground color should be dark
        print(f"color:{new_hex}", end="")
    elif re.match(COLOR_RE, chunk) and chunk.startswith("background-color:"):
        hex = chunk[len("background-color:") :]
        new_hex = hsl_clamp_l(hex, 0.6, 1)  # background color should be light
        print(f"background-color:{new_hex}", end="")
    else:
        print(chunk, end="")

This works specifically with the output of ANSI HTML adapter (aha), which I assume to be similar to ansi2html. Looping over re.split is just a nice way to make a traditional unixy pipeline tool.

Here are some examples of syntax highlighted git diffs with two different syntax themes. Sorry they're small, you can click each image to open it in a new tab:

Visual Studio Dark+Solarized
original
white background
white background with clamped L

ANSI generated with delta:

git diff | delta --color-only --syntax-theme='Solarized (dark)' | aha --black
git diff | delta --color-only --syntax-theme='Solarized (dark)' | aha
git diff | delta --color-only --syntax-theme='Solarized (dark)' | aha | python ./dark2light.py
git diff | delta --color-only --syntax-theme='Visual Studio Dark+' | aha --black
git diff | delta --color-only --syntax-theme='Visual Studio Dark+' | aha
git diff | delta --color-only --syntax-theme='Visual Studio Dark+' | aha | python ./dark2light.py

simonLeary42 avatar Aug 24 '25 22:08 simonLeary42

also note: colorsys is in the python standard library

simonLeary42 avatar Aug 24 '25 22:08 simonLeary42