Using UnicodePlots for pretty printing a type

Hi All,

We got a julia struct that represents a sparse matrix. In the literature this matrix is often represented with its plot (like with imshow) because it only has 0s or 1s.

We want to define custom show methods for it. I realized I can use UnicodePlots to overload show to display the unicode plot instead.I have achieved that, so for the following let’s imagine that I am able to define show for my type that uses UnicodePlots.

Here are my questions:

  1. I still want to have a “short” definition of my type, to be used in tuples/arrays. Julia manual says that I define show(io, mytype) for this short definition and then show(io, ::MIME"text/plain", mytype) for the long definition. Is this actually correct? How does “text/plain” correspond to all platforms that UnicodePlots supports? I mean, I could show this unicode plot to jupyter, juno, repl, everywhere. Does text/plain correspond to the type I should be using? ANSWER: Yes, I just use text/plain.

  2. Can I somehow deduce the character limit of the io ? How many characters can be fit in the single line? If yes, where is this in the documentation?

  3. Is there a way to “detect” the character spacing and the line height from the io ? By this I mean, how many pixels on the screen does the width of one character span, and how many pixels does the height of a line span.

  4. Lastly, is it possible to “deduce” if the io supports unicode characters or only ascii? Because UnicodePlots allows you to use only ascii if need be!

P.s.: This is how the matrix looks now when UnicodePlot-printed:

4 Likes
julia> displaysize(stdout)
(30, 120)
2 Likes

There was recently a thread on a related topic
https://discourse.julialang.org/t/how-to-detect-in-show-that-you-are-computing-display-for-the-repl/18951
The conclusion is that you check :limit to know if you prepare printing to the REPL.

I remember this is spy(H) in MATLAB with H being a sparse binary matrix (having only zeros and ones). Perhaps one of the plotting packages can implement this (if it is not already implemented)?

It is already available via Plots.jl (spy(H)). But I did not try it with UnicodePlots.

2 Likes

YES! I have developed an ingenious solution for this. Here’s how:

  • Draw a bunch of random green and blue blocks in the terminal using Unicode Full Block characters (‘█’).
  • Make one of these blocks red.
  • Flush the stdout.
  • Short sleep, this is important otherwise it won’t work (0.1 seconds seems enough).
  • Now capture the screen. On Windows this is performed with ccalls to Windows GDI.
  • Now we will search the screen for anything that’s red. Note: There may be false positives.
  • For any potentially matching red pixels, expand the area to find the size of that block.
  • Once you’ve found the size, search all around the red block to see if the surroundings exactly match the random green and blue blocks that you previously drew.
  • Once everything matches, you have successfully detected the character dimensions.
  • Finally, we will tidy up by using ANSI escape codes to erase all the blocks drawn.


And here’s the code (note: Windows only):

const USER, GDI =  "user32.dll", "gdi32.dll"

function cc(library, method, args...)
    argtypes = Expr(:tuple, (args .|> a -> isbits(a) ? UInt32 : Ptr{UInt8})...)
    @eval ccall(($method, $library), UInt32, $argtypes, $(args...))
end

R = (ansi = 91, matches = (r,g,b) -> r > g+32 && r > b+32)
G = (ansi = 92, matches = (r,g,b) -> g > r+32 && g > b+32)
B = (ansi = 94, matches = (r,g,b) -> b > r+32 && b > g+32)
BLOCKS = (b = rand([G B], 3, 10); b[2,2] = R; b)
XRES, YRES = Tuple(0:1 .|> m -> cc(USER, "GetSystemMetrics", m))

function print_blocks()
    S = BLOCKS .|> b -> "\e[1;$(b.ansi)m█"
    print.(permutedims(hcat(S, [fill("\n", size(BLOCKS,1)-1); " "]), [2,1]))
    flush(stdout)
end

erase_blocks() = print("\e[2K\e[A"^(size(BLOCKS,1)-1), "\e[2K\e[G\e[m")

function get_screen_pixels()
    hdc_screen = cc(USER, "GetDC", 0)
    hdc_mem = cc(GDI, "CreateCompatibleDC", hdc_screen)
    hbitmap = cc(GDI, "CreateCompatibleBitmap", hdc_screen, XRES, YRES)
    cc(GDI, "SelectObject", hdc_mem, hbitmap)
    cc(GDI, "BitBlt", hdc_mem, 0, 0, XRES, YRES, hdc_screen, 0, 0, 0xcc0020)
    pixels = Vector{UInt32}(undef, XRES * YRES)
    bmi = UInt32[40, XRES, -YRES, (32<<16) + 1, 0, 0, 0, 0, 0, 0]
    cc(GDI, "GetDIBits", hdc_mem, hbitmap, 0, YRES, pixels, bmi, 0)
    pixels
end

function search_blocks(pixels)
    px(x, y) = pixels[x + XRES*(y-1)]
    rgb(x, y) = (c = px(x, y); ((c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff))
    block_matches_pixel(c, r, x, y) = BLOCKS[r,c].matches(rgb(x, y)...)
    block_matches(c, r, x, y, w, h) = all(1 ≤ u ≤ XRES && 1 ≤ v ≤ YRES &&
        block_matches_pixel(c, r, u, v) for u = x:x+w-1, v = y:y+h-1)

    for y = 2:YRES-1, x = 2:XRES-1
        # test if the 4 pixels around (x,y) match the top-left 4 blocks
        if all(block_matches_pixel(2-j, 2-k, x-j, y-k) for j=0:1, k=0:1)
            # match found! now calculate block size (w,h)
            w = h = 1
            while x+w ≤ XRES && block_matches_pixel(2, 2, x+w, y) w += 1 end
            while y+h ≤ YRES && block_matches_pixel(2, 2, x, y+h) h += 1 end

            # finally test if ALL blocks match color and block size
            w > 2 && h > 2 && all(block_matches(j, k, x+(j-2)*w, y+(k-2)*h, w, h) for j=1:size(BLOCKS,2), k=1:size(BLOCKS,1)) &&
                return (w=w, h=h)
        end
    end
    nothing
end

function get_character_dimensions()
    print_blocks()
    sleep(0.1) # short delay to ensure output is really flushed
    pixels = get_screen_pixels()
    erase_blocks()
    dims = search_blocks(pixels)
end

dims = get_character_dimensions()

println("Character dimensions: $(dims ≡ nothing ? "not found" : "$(dims.w) x $(dims.h) pixels")")
7 Likes

this is so impressive, I am mindblown!!!

1 Like