okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
4db295e
main
1"""Draw a screen's ANSI text as a PNG, so a person or a model can look at it. 2 3TUI tests write what a view draws to $ALBEDO_SHOT_DIR/<name>.ans (see shot in 4cli/internal/tui/shot_test.go). This turns every .ans in a directory, or the 5files named, into a .svg and a .png beside it. Needs rsvg-convert. 6 7ALBEDO_SHOT_DIR=/tmp/shots go -C cli test ./internal/tui -run TestShotGallery 8python3 test/manual/tui_shot.py /tmp/shots 9 10For a directory it also makes contact sheets per screen, sheet-<screen>-<page>.png, 11six states to a sheet so a model can read them (images over 2000px a side are 12refused). The screen is the file name up to the first "-". Screens register 13their states with registerShots in the tests. 14""" 15 16import html 17import re 18import subprocess 19import sys 20import unicodedata 21from pathlib import Path 22 23BG ,FG = "#1e1e2e" ,"#cdd6f4" 24SHEET_TILES = 6 # two columns of three: a sheet stays under a model's 2000px edge limit 25FONTS = ["/System/Library/Fonts/Menlo.ttc" ,"/System/Library/Fonts/Monaco.ttf" ] 26BASIC = { 2730 :"#45475a" ,31 :"#f38ba8" ,32 :"#a6e3a1" ,33 :"#f9e2af" , 2834 :"#89b4fa" ,35 :"#cba6f7" ,36 :"#94e2d5" ,37 :"#bac2de" , 2990 :"#585b70" ,91 :"#f38ba8" ,92 :"#a6e3a1" ,93 :"#f9e2af" , 3094 :"#89b4fa" ,95 :"#cba6f7" ,96 :"#94e2d5" ,97 :"#a6adc8" , 31}# fmt: skip 32TOKEN = re .compile ( 33r"\x1b\[([0-9;:]*)m|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b_.*?\x1b\\|(.)" ,re .S 34) 35 36Style = dict [str ,str | bool | None ] 37 38 39def xterm (n :int )-> str : 40if n < 8 : 41return BASIC [30 + n ] 42if n < 16 : 43return BASIC [82 + n ] 44if n < 232 : 45n -= 16 46level = lambda v :0 if v == 0 else 55 + 40 * v # noqa: E731 47return "#%02x%02x%02x" % (level (n // 36 ),level (n // 6 % 6 ),level (n % 6 )) 48v = 8 + 10 * (n - 232 ) 49return "#%02x%02x%02x" % (v ,v ,v ) 50 51 52def fresh ()-> Style : 53return { 54"fg" :None , 55"bg" :None , 56"bold" :False , 57"faint" :False , 58"under" :False , 59"reverse" :False , 60 } 61 62 63def apply (style :Style ,params :list [int ])-> None : 64i = 0 65while i < len (params ): 66p = params [i ] 67if p == 0 : 68style .update (fresh ()) 69elif p in (1 ,2 ,4 ,7 ): 70style [{1 :"bold" ,2 :"faint" ,4 :"under" ,7 :"reverse" }[p ]]= True 71elif p == 22 : 72style .update (bold = False ,faint = False ) 73elif p == 24 : 74style ["under" ]= False 75elif p == 27 : 76style ["reverse" ]= False 77elif p == 39 : 78style ["fg" ]= None 79elif p == 49 : 80style ["bg" ]= None 81elif p in (38 ,48 ): 82key = "fg" if p == 38 else "bg" 83if params [i + 1 ]== 2 : 84style [key ]= "#%02x%02x%02x" % tuple (params [i + 2 :i + 5 ]) 85i += 4 86elif params [i + 1 ]== 5 : 87style [key ]= xterm (params [i + 2 ]) 88i += 2 89elif 30 <= p <= 37 or 90 <= p <= 97 : 90style ["fg" ]= BASIC [p ] 91elif 40 <= p <= 47 or 100 <= p <= 107 : 92style ["bg" ]= BASIC [p - 10 ] 93i += 1 94 95 96def width_of (char :str )-> int : 97if unicodedata .combining (char ): 98return 0 99return 2 if unicodedata .east_asian_width (char )in "WF" else 1 100 101 102def to_svg (text :str ,cw :int = 9 ,ch :int = 19 ,pad :int = 18 ,size :int = 15 )-> str : 103cells :list [tuple [int ,int ,str ,Style ,int ]]= [] 104lines = text .rstrip ("\n" ).split ("\n" ) 105columns = 0 106for y ,line in enumerate (lines ): 107style ,x = fresh (),0 108for match in TOKEN .finditer (line ): 109if match .group (2 )is not None : 110w = width_of (match .group (2 )) 111cells .append ((x ,y ,match .group (2 ),dict (style ),w )) 112x += w 113elif match .group (1 )is not None : 114apply ( 115style , 116 [ 117int (p )if p else 0 118for p in re .split ("[;:]" ,match .group (1 )or "0" ) 119 ], 120 ) 121columns = max (columns ,x ) 122out = [ 123f'<svg xmlns="http://www.w3.org/2000/svg" width=" { pad * 2 + columns * cw } " height=" { pad * 2 + len ( lines ) * ch } ">' , 124f'<rect width="100%" height="100%" rx="10" fill=" { BG } "/>' , 125f'<g font-family="Menlo" font-size=" { size } " xml:space="preserve">' , 126 ] 127for x ,y ,char ,style ,w in cells : 128fg ,bg = style ["fg" ]or FG ,style ["bg" ] 129if style ["reverse" ]: 130fg ,bg = bg or BG ,fg 131if bg : 132out .append ( 133f'<rect x=" { pad + x * cw } " y=" { pad + y * ch } " width=" { cw * w + 0.5 } " height=" { ch + 0.5 } " fill=" { bg } "/>' 134 ) 135if char == " " : 136continue 137extra = (' font-weight="bold"' if style ["bold" ]else "" )+ ( 138' opacity="0.6"' if style ["faint" ]else "" 139 ) 140extra += ' text-decoration="underline"' if style ["under" ]else "" 141out .append ( 142f'<text x=" { pad + x * cw + cw * w / 2 } " y=" { pad + y * ch + ch * 0.75 } " text-anchor="middle" fill=" { fg } " { extra } > { html . escape ( char ) } </text>' 143 ) 144out .append ("</g></svg>" ) 145return "\n" .join (out ) 146 147 148def main (args :list [str ])-> int : 149sources = [ 150p 151for a in args 152for p in (sorted (Path (a ).glob ("*.ans" ))if Path (a ).is_dir ()else [Path (a )]) 153 ] 154if not sources : 155__doc__ ,file = sys .stderr ) 156return 2 157sheets :dict [tuple [Path ,str ],list [Path ]]= {} 158for source in sources : 159svg ,png = source .with_suffix (".svg" ),source .with_suffix (".png" ) 160if not png .exists ()or png .stat ().st_mtime < source .stat ().st_mtime : 161svg .write_text (to_svg (source .read_text (encoding = "utf-8" )),encoding = "utf-8" ) 162subprocess .run (["rsvg-convert" ,str (svg ),"-o" ,str (png )],check = True ) 163screen = re .split ("-" ,source .stem ,maxsplit = 1 )[0 ] 164sheets .setdefault ((source .parent ,screen ), []).append (png ) 165for (folder ,screen ),pngs in sheets .items (): 166font = next ((f for f in FONTS if Path (f ).exists ()),None ) 167montage = ["magick" ,"montage" ,"-pointsize" ,"16" ,"-fill" ,"#cdd6f4" ] 168montage += ["-font" ,font ,"-label" ,"%t" ]if font else ["-label" ,"" ] 169montage += [ 170"-background" , 171"#11111b" , 172"-tile" , 173"2x3" , 174"-geometry" , 175"970x>+12+12" , 176"-depth" , 177"8" , 178 ] 179for page ,start in enumerate (range (0 ,len (pngs ),SHEET_TILES ),1 ): 180sheet = folder / f"sheet- { screen } - { page } .png" 181subprocess .run ( 182 [* montage ,* map (str ,pngs [start :start + SHEET_TILES ]),str (sheet )], 183check = True , 184 ) 185sheet ) 186return 0 187 188 189if __name__ == "__main__" : 190sys .exit (main (sys .argv [1 :]))