├── example ├── .DS_Store ├── jiaozhu.jpg ├── ansi-terminal.png ├── without-color.html └── without-color-dither.html ├── .gitignore ├── setup.py ├── graphics_util.py ├── LICENSE ├── Readme.md ├── img2txt.py └── ansi.py /example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hit9/img2txt/HEAD/example/.DS_Store -------------------------------------------------------------------------------- /example/jiaozhu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hit9/img2txt/HEAD/example/jiaozhu.jpg -------------------------------------------------------------------------------- /example/ansi-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hit9/img2txt/HEAD/example/ansi-terminal.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | img2txt.py.egg-info 3 | dist 4 | *.pyc 5 | *.swp 6 | *.swo 7 | .ropeproject 8 | venv 9 | test 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="img2txt.py", 5 | version="2.4", 6 | author="hit9", 7 | author_email="hit9@icloud.com", 8 | description="Image to Ascii Text, can output to html or ansi terminal.", 9 | license="BSD", 10 | url="https://github.com/hit9/img2txt", 11 | install_requires=['docopt', 'Pillow'], 12 | scripts=['img2txt.py'], 13 | py_modules=['ansi', 'graphics_util'], 14 | ) 15 | -------------------------------------------------------------------------------- /graphics_util.py: -------------------------------------------------------------------------------- 1 | def alpha_blend(src, dst): 2 | # Does not assume that dst is fully opaque 3 | # See https://en.wikipedia.org/wiki/Alpha_compositing - section on "Alpha Blending" 4 | src_multiplier = (src[3] / 255.0) 5 | dst_multiplier = (dst[3] / 255.0) * (1 - src_multiplier) 6 | result_alpha = src_multiplier + dst_multiplier 7 | if result_alpha == 0: # special case to prevent div by zero below 8 | return (0, 0, 0, 0) 9 | else: 10 | return ( 11 | int(((src[0] * src_multiplier) + (dst[0] * dst_multiplier)) / result_alpha), 12 | int(((src[1] * src_multiplier) + (dst[1] * dst_multiplier)) / result_alpha), 13 | int(((src[2] * src_multiplier) + (dst[2] * dst_multiplier)) / result_alpha), 14 | int(result_alpha * 255) 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2016, hit9 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of img2txt nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | img2txt 2 | ======= 3 | 4 | Image to Ascii Text, can output to html or ansi terminal. 5 | 6 | See also [gif2txt](https://github.com/hit9/gif2txt) for animated version. 7 | 8 | Example 9 | ------- 10 | 11 | ![](example/jiaozhu.jpg) 12 | 13 | 1. `img2txt.py jiaozhu.jpg > without-color.html` : [demo](http://hit9.github.io/img2txt/example/without-color.html) 14 | 2. `img2txt.py jiaozhu.jpg --dither > without-color-dither.html` : [demo](http://hit9.github.io/img2txt/example/without-color-dither.html) 15 | 3. `img2txt.py jiaozhu.jpg --color > with-color.html`: [demo](http://hit9.github.io/img2txt/example/with-color.html) 16 | 4. `img2txt.py jiaozhu.jpg --ansi`: [demo](http://hit9.github.io/img2txt/example/ansi-terminal.png) 17 | 18 | Installation 19 | ------------ 20 | 21 | ```bash 22 | $ virtualenv venv 23 | $ . venv/bin/activate 24 | (venv)$ pip install img2txt.py 25 | ``` 26 | 27 | Usage 28 | ----- 29 | 30 | ``` 31 | Usage: 32 | img2txt.py [--maxLen=] [--fontSize=] [--color] [--ansi] [--bgcolor=<#RRGGBB>] [--targetAspect=] [--antialias] [--dither] 33 | img2txt.py (-h | --help) 34 | 35 | Options: 36 | -h --help show this screen. 37 | --ansi output an ANSI rendering of the image 38 | --color output a colored HTML rendering of the image. 39 | --antialias causes any resizing of the image to use antialiasing 40 | --dither dither the colors to web palette. Useful when converting 41 | images to ANSI (which has a limited color palette) 42 | --fontSize= sets font size (in pixels) when outputting HTML, 43 | default: 7 44 | --maxLen= resize image so that larger of width or height matches 45 | maxLen, default: 100px 46 | --bgcolor=<#RRGGBB> if specified, is blended with transparent pixels to 47 | produce the output. In ansi case, if no bgcolor set, a 48 | fully transparent pixel is not drawn at all, partially 49 | transparent pixels drawn as if opaque 50 | --targetAspect= resize image to this ratio of width to height. Default is 51 | 1.0 (no resize). For a typical terminal where height of a 52 | character is 2x its width, you might want to try 0.5 here 53 | ``` 54 | 55 | Authors 56 | ------- 57 | 58 | - @EdRowe (#4, #7) 59 | - @shakib609 (#10) 60 | - @mattaudesse (#11) 61 | - @hit9 62 | 63 | License 64 | ------- 65 | 66 | BSD. 67 | -------------------------------------------------------------------------------- /example/without-color.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 |
??????????C???????CCC?CCCCCCCCCC?CCC?CCCCCCC??C?????????CCCCCCCCCCCC?C??C???CCOCC?CC??CCCC??CC??????
20 | ?7???????????????????????????C?????????CCCC?????????????CCCCCCCCCCCCC???????COCCCCCC??CCC??????C???C
21 | 77?????????????????????????????????????CCCC????????????CCCCCCCOCCCCCC???????COOOCCC?C?CCC????????CCC
22 | ?7?????????????????????????????????????COCCCCC????C????CCCCOOOOOCCCCC???CCCCCOOOOCCCCCCCC???????CCCC
23 | ?7????????????????????????????????????CCCCOCCCCCCCCCCCCCCCCOCOOOOCCCC??CCCCCOOOOOCCOCCCCCC??????CCCC
24 | ???????????????????CC???C???CC???????CCCCOCCCCCCCCCCCCCCCCCOCOOOOCCCCCCCCCCCCOOOOCOOOOCCCCCC??CCCCCC
25 | ?????????????CCCCCCCCCCCC???CCCC??CC?CCCCCOOCCCCCCCCCCCCOCCCCCCCCCCCCCCCCCCCOOOOOCOOOOCOCCCC???CCCCC
26 | ????????????CCCCCCCOOOCCCCCCCCCCCC?CCCCCOCCCOCCCOCCOCOCCOCCOCCOCCCCCCCCCCCCCCOOCCCCCCCCCCCCCCCCCCCCC
27 | ????????????CCCCOOOOOOCCCCCCCCCCCCCCCCCCOOCCCC$$$$O$O$$OOOOOCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
28 | ???????????CCCCCOOOOOOCCCCCCCCCCCCCCCCCCCOCO$QQ$QQQQQQHHHQ$OOOOOOOOO$OOOCCCCCCCCCCCCCCCCCOCCCCCCCCCC
29 | CC?????CC?CCCCCCOOOOOCCCCCCCCCOOOCCCCCCCCOOQHMMNMMNNHNNNMNNHQ$$OO$$OO$$O$Q$CCCCCCCC?CCCCCCCCCCCCCCCC
30 | CC?CC??CCCCCCCCOOOOOOOCCCCCCCCOOOCCCCCCCCO$HHNNMNMMMMMMMNNNHHHQHQQQH$$$OO$$Q$CCCCCCCCCC?.      .7COC
31 | CCC?CCC??CCCCCCCOOOOCCCCCCCCCOOOOOCCCCCOO$$QQQHHNMMMMMNNHHHNNMNNHQQQHHQQQO$$OQCC?CCC7-          .-?C
32 | CCCCCCCCCCCCCCCCOOOCCCCCCCCCCCCOOCCCCCO$$QQQQQQHNNMNNQ$QHHMMMMNMNNHHHHHHQQQQQQ$?CC.         ...     
33 | CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCO$$QQQQQQQQQHHQQQQHNNHNNNNMMMMNHHHHNHHHQQHH?7;         .;;.     
34 | CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC?CCC$QQQHQHHQQ$QQHQQQNHHHNHHQHHNMMMMNNNNHHHHQQQ>!:..      .;;.     ;-
35 | CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC?CO$QQQNQHHHQQQ$QHQ$QNHNNHQQHHHNNNMMNNHHHHHHHHNQC>-....   ..;.    .;-.
36 | CC??CCCCCCCCCCC?C?C?CCC?CCCCO$QHHQQQHHQNHQQQQQHQQNHHN$QHHHNQHMMMMMMNNNNNNHNNNMQ>;..... .....   .:;..
37 | CCC?CCCCCCCCCC???????????C$QHHQHQQNNNNNQQQHHHQQHHNHQQQQQQHHNHNNNMMNNNNNNNNMMMNH7;.......;..   .-;...
38 | ?????CCCCCC?C??????C???COQ$$QHHHQNNNMNHQQQHHHHQHMQQQQQQHHNMMMNMMMNNNNNNNNMMMMMH?-......;.... .-;....
39 | ????C?C???????????????OQQ$$$HHHQMNMMNNHHHHHQHQHNQHNHQNNMMMMMMMMMMMNNNNNNNMMMMMMO>....;;;.....;......
40 | ?????????????????????O$O$$HHNHNMMMMMNNHHNHNQQHNQNNNNMNNNNNMMMNNNNNNMMMNMMMMMMMMQC...;;:;;.... .  ...
41 | ??????????????7777??OO$$QNNNNNMMMMMMMHNNQHQQHNNNNNMMNHNMMMNNHQQQQNNNNMMMNMMMMMNH$;.;;--;....    ....
42 | ???????7????777777?OOO$HNNNNNMMMMMMMNHNNHQHHNNNNNMMNNNMNHQQOOC?CC$HNNMMMMMMMMMNHQ>;;;--;...  ..... .
43 | ???777777???7777??Q$$QHNNMNNMMMMMMMMNHNHHHQHNNHNMMMMMNH$CCC??7777?OHNNNMMMMMMMMNHC----;.;. .........
44 | ?77777777777??7?C$?>?QNNMNMMMMMMMMMMNNNHQHNNNNNMMMMMN$C??777>>>>>7?OQNNMMMMMMMNNNQ;;-;.;; ..... ....
45 | 777777777777>:---:::!7HNNNMNNMMMMMMNNNNHHNNNNMMMMMMHOC?7777>>>>>>>7?CQNMMMMMMMMNNN;--.;-;...........
46 | 77777777!-------::::!7OHHNMMMMMMMMMNMNNNHNNMMMMMMMN$C?7>7>>>!>!>>>77?OQMMMMMMMMMNN;-;;--;...;......;
47 | 777!-;-------:::::!!>7OHQNMMMMMMMMMNMNHHNMNMMMMMMNHC?7>>>>!!!!!!>>>77COHMMMMMMMNHN;:--:!;..;.......;
48 | ;;---------::::::!!>7C$HNNMMMMMMMMMMMNMQMMMMMMMMMQC?7>>>>!!!!!!!!>>>7?CQMMMMMMMNNQ-:--!!;;;;.......;
49 | ;;--------:::!!!>>77COQHNNMMMMMMMMMMMMMHMMMMMMMMHC?7>>>>>!!!!!!!!>>>77?ONMMMMMMMN:---:>:;;;.... ...;
50 | ---------::!!!!>>77CO$HNNNNMMMMMMMMMMMMHMMMMMMMNC?77>>>>>!!!!!!!!!>>77?CNMMMMMMMN---:!7;;;.........;
51 | --------:::!>>>77?CO$QHNNMNMMMMMMMMMMMMNMMMMMMNO?777>>>!>!!!!!!!!!>>77?CQNNMMMMN!---:7>;;...........
52 | -----:::!!>>>>7???O$QHNMMMMMMMMMMMMMMMMMMMMMMM$C?77>>>>!!!!!!!!>>>>7?C$CONNMMMM?:-::-?-...........;;
53 | ---::::!!>>777??CO$HNNMMMMMMMMMMMMMMMMNMMMMMMNC?77>>>>>>>>>>>>>>>>CO$$QHCHNMMMMC!::::-;..... .....;;
54 | -:::::!>>>777??CO$HNMMMMMMMMMMMMMMMMMMMMMMMMN$C??>>>>>>>>>>>>>>7?$QHQ$OO$QNMMMMH>:::::...........;;;
55 | .;:::!!>>77??COQHNNMMMMMMMMMMMMMMMMMMMMMMMMN$O??CCCO$$QO?7>>>>7?CO???????OHMMMMNC!::::;.........;;;;
56 | ....:!>>7??CC$QHH$OHMMMMMMMMMMMMMMMMMMMMMMQOCC?$Q$QHHQQ$OC?>>>7???77?CCC?CQMMMMNNC!!!:..........;;;;
57 |    ..->7??CO$O7$$OCNMMMMMMMMMMMMMMMMMMMMMQCC??$QO??????CC?77>>77?777C$$C??QMMMMNHC>>!-.........;;;;;
58 | .. ....?CC$C???$C?CNMMMMMMMMMMMMMMMMMMMMNO??????777>7?7?C??>>!>7??$$QMNQ??$MMMM$CC?7>;.........;;;;;
59 | ... .;;.7??????O??ONMMMMMMMMMMMMMMMMMMMMNC??7?77?????COC???>!!>??O7>NHQ?77$NMMHO?C???;  ...;...;;;;;
60 | .....;;;-!7777?C?CCNMMMMMMMMMMMMMMMOMMMMMC?777??CO$N$CQNQ7>>!:!7>???C??777ONNN$OCC???-....;....;;;;;
61 | .;....;;-:>7777?C?ONMMMMMMMMMMMMMMNNMMMMMC77777?ONN$7CNNH>>>!:!>>7??C77777CNMN$CCC?C?!...;;;..;;;;;;
62 | -..;;;;-:!7??777??CNMMMMMMMMMMMMMMNNMMMMMO777>>7>?CC????7>!!!:!>>>>>>>>>>7CMMNQCC?????;.;;;..;.;;;;;
63 | !;;;;;-::7????????OHMMMMMMMMMMMMMMMMMMMMM$777>>>>>7????7>!!!!::!>!!!!!!>>7?MMMHOC?????:;;;;....;;;;;
64 | .;;;-;:!>7????????CHNMMMMMMMMMMMMMMMNMMMMQ?77>>>>>>!>>>!!!!!!!:!>!!!!!!>>7?NMMN$CC????!-;;...;.;;;;;
65 | >:-;;.!>7?????????C$MMMMMMMMMMMMMMMNNMMNNH?77>>>!!!::!!:!!!!!:::!!!!!!!>>7?MNMNQC?????:-;;;...;;;;;;
66 | -:-;;-7??777???????ONMMMMMMMMMMMMMM$QNHHHH??7>>>!:::::::!!!!!:::!>>>!!!>>7ONMMN$CC???7---;;;;;;;;;.;
67 | :..;;777777777?????CNMMMMMMMMMMMMMMO$HQQ$HC?77>>!!:::::!!>>!>:::!>77>>>>7>QMMNNHO??7?7;;-;;;;;;;;;..
68 | :>-:>7777777777?7??CNMMMMMMMMMMMMMMMCO?$OQO??7>>>!!!!:!!>7>!>>!>!>7?77>777HMMMNHOC?7?!;;;;;;.;;;;...
69 | ;-::77777777777777?C$HNMMMMMMMMMMMMMOCC?COO??777>>!!!!!>?C7>77>>>?>7?77777NMMMNN$??77:;;;;;;.;;;;...
70 | ;;-;;:>>777?????????ONNMMMMMMMMMMMMMM?CCCCC?7777>>>>>>>??7?OOO?7?7>>>77777NMMMNNHC?7?;..;;;.;;;;;...
71 | ....;-;!7777????????$QNMMMMMMMMMMMMMMM????C?7777777>>7777>>7????77>>>>777?MMMMHHHO??7;;...;.;;;;;...
72 | .;.;.;-;!7?77?????C?Q$NMMMMMMMMMMMMMMMMNO?QC?77777777777>>>>>>7>>!>>77777CNNNMQHNH?77;.....;;;;;....
73 | .;.;.;;-;!77?????CC?$QHMMMMMMMMMMMMMMMMMMMM?777777777777>>!!!>>7????7777>$MMNHQQNHO77;.....;;;;;....
74 | ;;...;;;-;:777??????C$HNNMMMMMMMNNMMMMMMMMMQ?77777777>77>>7??CCCCOO$?7>7>HHNH$O$QOO?7.....;;;;;;...;
75 | ;;.....;;;;-7777?7??CQ$QNNMMMMMMM$NMMMMMMMMMC?77>77>>>7?O$$OCCC?7??777>>7NNHQHC$CCCC>;....;;;;;;..;;
76 | ;.......;;;.77777777?$Q$NNNMMMMMM$$MNMMMMMMM??77>>>>>>7777???777??777777CMNOO$O$CCCC-.;..;;;;;;..;;;
77 | ............;7777777?COQNNNMMMMMMHONMMMMMMMMO?7777>>77777777??????77>777HMMH$O$$OCC?;....;;;;;;..;;;
78 | ......;;.....;..->777COQNNMMMMMMMH$QMMMMMMMMN?7777>7>>7777777???77>7>>7OMMMMOONOOCC>.;...;;;;;;..;;;
79 | ......;;.....;;.;;;-7?OHNHMMMMMMMHQHNMMMMMMMM?7777777777>>>777>>>>>>>77NMMMNNMO$CCC-....;;;;;;....;;
80 | ......;;;....;..;;;-!?CHQNNNMMNMMHN$HMMNMMMMM7777777>>>>>>>>>>>>>!>>>7HNHMNNNQOCCOO;;...;;;;;...;;;;
81 | ......;;......;.;:;;:7?$$NHNMMNMNHQNQHNHHNMMMC777777>>>>>>!!!!!>>!>>>$HNMNNOOCCC$CC.;;....;;;..;;;;;
82 | .....;;;......;..;;.:?7?CQNMNMMNHNNNHMMNHQHMMN777777777>>>>!!!>>>>>>C$QHCC??????CC?.;;....;;....;;;;
83 | .....;;;;.....;.;;;;:>7>?>?$NNNMQHHCCC?O$COQQN77777777777>>>>>>>>>>>CC?7??????????!.;...........;;;;
84 | ......-;;.....;...;->:>>77?HHMOH$$$$???CO???C?777777777777>>>>>>>>7??O>?????????77;.;...........;;;;
85 | ......;;;;....;...-:-!7>O?OHHHOQC7??C???C?7??77777777777777777????777$!;7????????!...;;...;.....;;;;
86 | ......;;;;;...;..;;:;-!!O?OHN$CO?7????????7?7777777777777777777777777O-;.7??????>;.....;.;.....;;;;;
87 | ..;;;.;;;;....;...;;;:!!C?$HN???7777>!7???777>>7777777777777777777777!-;;-?????>-......;.......;;;;;
88 | ;;;;;.;;;;;...;.....--!!C7$HN7>7>!7?>!>???7>::!>77777777777777777777?-;;;;-???7--......;......;;;;;;
89 | ;;.;;.;;;;;........;;;!:>COH?!!>!!77!!:>7>::-;:>7777777777777777777?:;;;;;;;77;-.......;......;;;;--
90 | ;;...;;-;;...........;:-!>?!::!!:!!::::!!::-;.->777777777777777777?!-;;;;;;;;;;;.. ....;....;;;;;;;;
91 | ;....;;;;;;....;.....;-;-----:::::-::::7:--;. ->777777777777777777!-;;;;;;;;;.;.............;;;;;;;;
92 | ;......;;;;....;.....;-;;:;;;------:::!!--;.. ->77777777777777777>-;;;;;;;;;..;.. .........;;;;;;;;;
93 | -;;....;;;;....;;....;;;.-;;;;;;;;---:!:-;;...;>7777777777777777>:;;;;;;;;;...... ........;;;;;;;-;;
94 | --;;;...;;;....;;....;;..;..;;;;;;;--...;;....;!777777777777777>:;;;;;;;;;...............;;;;;;;;;;;
95 | 
96 | 97 | 98 | -------------------------------------------------------------------------------- /example/without-color-dither.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 |
?7??7?????C???7?C??C???C??CCC??C??C???C?CC?C??C???????C?C?C?C?CCC?C?C??C??C?COCC?C?C??C?C???C??C?C?C
20 | 7????7C7C??7C7??7???7C???????C??????C??OCC??C???C?C?C???C?C?CCOCOO?C?C??C???COCOC?CC??CC?C??7CC??7C?
21 | 7?7?7?7??7C?7?7C??????7?C7?C?C??7C?7??C?CCC??C??????7C?C?C?OCCCC?C??C?C??C??COOCCC?7O?C?CC?C??7?CCC?
22 | ?7?7???????7?C?7???7???C???7?C7C????C??COO?C?C?CC7C?C??C?CCCCO$OO?C?C??C??CCOC$COCOC?O?CCC7???C???O?
23 | 7?7?7?????C??7???7?C?C???7??C?????7??CCCCCO?C?C??C?C?C?C?OCOOCCOOC?C?C??CC?O?OOOCO?OCO?O?C?CCC7O?CC?
24 | ?7??????C7?7C?C?C??C????CC???O???C?C??C?OCOO?C?CCO?O?CCC?OCCCOOOCOC?C?C?O??COOOOOCOCCCOC?O??C?C?O?CO
25 | ?????7???C????C??C?O?O?C??C?C??C??C?CCCOO?OOOCO??CCCOCCCOCCCOCCCCO?OCCCC?COCCOCOCCOOOCCCO?CC7C?CCO?C
26 | ?7????C???C?CC?CO?COCCO?C?C?CCCCCC?C?C?OOCCCC?COCOCOCOCCOCCCC?OC?CCC?C?CO?CCCCO?CCCC?OCCCOCCO?C?C?OC
27 | ?????7?C7O??C?CCCOOOOCO?O?CC?C?C?CC?O?O?OO?COCOQOCOOOC$OOOOCOCOCOCOCOCC?C?C?COCCCC?CO?CO?CCC?O?OCCCO
28 | ???C?CC???CCCCCOCOOCOCO?C??CCCCO?O?C?CCO?CCO$Q$$QQNONQNQNO$COCCC$COOOOOOCO?CC?C?C?O?CCCCCOCO?CC?O?OC
29 | ?C??C?7CCC??C?O?OOOCOO?CCCC?CCCOOC?CCCCCOOOOHNMNNMNNQNHNNNNHQO$$O$$C$O$O$OOCCC?OC???CC?C?CC?COCOCCCO
30 | ?O?C???C??O?O?COOOOO?OC?CC?OCCOOOO?OO?O?COOHHNMMNMMMMNMMNNHHQN$Q$Q$H$$O$OO$HOCC?O?C??CC7;. .   .7OCO
31 | C?C?O?O???C??C?COCCOCOCC?CCCC$OO?OC?COCOOC$Q$QQQMHNNNMHHQNQNNMNNH$QQQH$Q$$O$O$O??C?C7-.    .  .  !7C
32 | CCC?C?C??O?CCCOCOOCO?CCCCCCCO?CCO?OC?OOC$N$$Q$$HHNNNHO$QHHMMNNNNNNNQN$NQ$H$Q$$OC?C;    ..   -. .  . 
33 | ?CCCCCCC?C?C??C?CCC?CCC?CO?C?O?$CC?OOOOH$QQH$QQ$HHQQQQNNHHNQNNNMNNQNQNQNHHQQ$NH77; ....   ..;- .    
34 | C?C?C?C?C?CCCC?O?C?O?CCOCCCO??C?C?OO$Q$HQQQ$Q$$QQQ$$NHQHHQQ$QQHMMMNNQNHH$HH$$Q>!:....    --;.. . .--
35 | ?CCC?O?O?C?C?O??CCC?CCC?C?CCC?C?O$$Q$NOHHHQ$H$QQH$QNQNNQQ$NQNNNNNMNNHHHHNQHNHQC>-.... . ..;.. . ;.-;
36 | C??C??CCCCCC??CCC?CC?C?C?O?CO$$NHQQQNQHN$OQQ$QH$QHNHHOQNQNHQHNMNMNMNNNHHHHNHNN$>;..;..; -.; . ..>;..
37 | ?CC?CC?O??C?O??7?????CC?CC$$HQ$H$QNNHNHOQNQNH$QNQNQ$QQ$Q$HHNQNQMNNHHHHNNNHNMMNH>-......;;.... ;-.-. 
38 | ????C??C?CC??CC??C7?C7?COQOQ$HHH$NNHNQNQ$$HQNQ$NNQ$H$QQQNQMNMNMNMNQNNNHHNNMNMMHC;......-.....;--....
39 | 7C7CC7O???C??7?7?7C??COQ$Q$$HHQQNMNMNNQHNQH$Q$HHONNQQNNMNMNMNMNMHMNNNHNNNMNMMNNC7.;..;--.....-. ....
40 | ?7??7??????7?C7?77??7O$O$OQNHQNMNMMNHNQHHQN$QNHQNQNNNNNHNHMNNNHNHHNNNNMNNNNMNNM$C..;;;!;;;.;. ...;. 
41 | 7C?C7C??7??7?77777C?$CC$QNNNHNNNMNNNMQNH$N$QQNHNMNMMHHHMNMHNH$$QQNNNMNNNNMNMNMNH$;.;---;............
42 | 7?77?777C777?77?77?OCO$NNQNQNMNNMNMMNQNNQQQHNNNQNMNHHNMNH$QOCC?CC$QNHNNMNNNNNMNN$>;----;.;. ..... ..
43 | 7?77?77777C7777??O$$$$NQNMNMMMNNMMNNNQNQQHQQMHHNNMNMMHQ$CCC?7C>>>?OHHMNMMNMMNMHNQO--;:;.;........; .
44 | 77777777?777C?7??$7>?HNNMNNNNNMMNMMNNNHHONNNQNNNMMNNH$C??7>>>>>>>7?C$NHNNMMNMNNNN$;---.-; .;.......;
45 | 7?77777777777:-!-:::>>HHHHMNNMNMMMNHNNHHHQNHNMMMMMMHCC77>>C>>>>>>>7CC$NMMNNMMNNNNN.--;;-;.;.;.; ..;.
46 | 777777?7>:------!-:!>7OHHMNNNMMMMNNNMHNNQMNMMNNMNNN$C77>>>>>>>!>>>>7?O$MNMNNMMMMQN---;-:;.;.;.;.;..;
47 | 777!!;;-----!-!:-!!:>7CNOMNMMMMNMNNNHNQQNNHMNMMNMHQC77>>>>>!!!>>>>>7?CCHNNMMNNNNHN;!--:>;.;-.......;
48 | ;---;--:--:-!-:!:>>>7?$HHNMNMNMNMMNMMNMQNMMMMMMMNQCC>>>7>>!>>>:>!>>7>?CQNMNMMMMHNQ-:-!!!.-.;.;....;-
49 | -;---:---!-!:>!>>>>7COQHHNNNNNMNNNMNNMNQMMNMMMMMHC?7>7>>>>!!>>>>>>>>77CCMNMNMNMNN:;-!->:;-;.;....;.;
50 | --:----!-::!:!>>>>?CC$HNNQNMMMNMMNMMNMMQMMMMMMMNC?7>>>>>>!>>!!::>!>>>??CNNNHMMNMH-!--!7-;.;......;;.
51 | ---;-!--!-!>>>>>7CCC$QHHNMHNNMMMNMNMNNMNMNMNNMQC?777>>>!>>:>>!>>>>>>777C$NNMMMNN!---!7>;-;;;...;;.;;
52 | --!--:::>>>>>>77C?COQHNNMNMNMMNNNMMMMMMHMMMMNN$C7>>7>>>>:>>!!>>:>>>?7C$CCNNNMNM7>;!!-?:......;.;.;;;
53 | -:-:::!!>>>>>?7CCC$HNNMNNMMNNMNMNMMNNMNNMMMMMN?C7?>>>>!>>>>>>>>>7>?$$$$H?NHMMNN?>!-!:--.-.......-.;.
54 | -:::!::>>>>77C7CO$HNMMNMMMMMMMNMMMNMNMMNMMMNH$CC7>77!7>7>>>>>!7>C$QQ$QCO$QNMNNMH!!-!-!.;......;..;;;
55 | .--!:>>>>>?7CC$$QNNMMNMMMMMNMNMMMMMMMMNMMMMQ$CC?CC?C$$Q$?7>7>>7C?O??C7C7CC$MMMNNC!!!-!;.....;..;-.-;
56 |  ...:!>>>?CCC$ONHCCHMMMMMMMNMMMMMMMMMMMNMN$OC??O$$$NQQ$$OC?>7>>CC7?7CCCC7CQNMNNNNC>!!!....;..;.-.;-.
57 |  ....->77C?O$O7OQOCNMMMMMMMMMMMMMMMMMMNMM$C?C?OQ$?7?7C?CC??>>>7>7C>??O$C??QMMNMHH?>!!;;....;.-.;.-.;
58 | ...;;.;?CCOC??COC?CNMMMMMMMMMMMMMMMMMMMMNCC?CC7C>77>7?7??C?>>>>C7COO$MN$C?$NNMNOCC777;...;.;.;;;;;--
59 | .....-..7C7?7??O?CONMMMMMMMMMMMMMMMMNMNMQ?C>>C>?C?C7?C$?C?7>>:>7?O7>NQ$C7>$NNMQO?O7O7;...;;;-.;;-.;-
60 | .;;..;;-->>7?77C?C?NMMMMMMMMMMMNMMNCNMMMNC77>7C7CC$N$C$NQ7>>>:>7>C7CC?C7>7$NNNOOCCC?7: .;.-.;.;;;--.
61 | ..;..;;.-:>7?7????ONMMMMMMMMMMMMMMQNNMNMNC?>?>>CONNO>CNNH7>>!:>>>7C7C7>>C>CNMNOCC??O7>..;;.;.;;.-;.-
62 | !.;-.-;-!>>?7?77??CNNHMMMMMMMMMMMMNNMNNMNC7?7>>>77CC??C?>>>!!-!>>>>>>>>>>7?MNNQCC??7?7-.---.;;;;;---
63 | >;;;--->->??C7?7?7OHMMMMMMMMMMMMMMNMNMNNN$77>>>>>>7C?C?>>!>>>:!>>:>:>:>>>7?NNNQOC7C7C7:--.;.;;;;--.-
64 | .-;--.!!>7?7?7?7??CHNMMMMMMMMMMMMMMNNHNNN$C>>7>>>>>!>!>!>:>:!!:!>>:!!!>!>>?MNMH$O??7C?!-;;;;.-..;;-.
65 | >!-;-.!>7?7?7????7COMNMMMMMMMMMMMNMHHMNNNQ?7?>>>!!!::!:>!:>!>:!:>>!!!!!>7>CMNMNOO??C77>;--.-..--;;-.
66 | -!--.-7?7?77?7?7???OHNMMMMMMMMMNMNNO$NHQQHC>7>7>!!::>->-:>:>>-!->>>>>!>>>>ONMNH$O?7??7-:--.-.;;;;.-.
67 | ! .-->?777?77?7??7?OHNMMMMNMMMMNMMN$$H$Q$HCC7>>>!!->-!->!>>>>:-!>>>>>>>>7>$MNNNHCC7?77;--;;-.-.;;-..
68 | !>!->77?7?77?7?7?7??HMNNNMNMNMMMMNMNCC?$OQ??C7>>>>!!!:!>>>>:>>>>:>7??>7>77NNNNNQO??7?>.-;;-;.-.-;.;.
69 | --!->?7?7777777?7?7OOHNMMMMMMMMMNNMMOCCCC$OC7?7>>>!!>!>>7C7>>7>>>C>>?>7>77NMNMHN$??77:-.;--;;;;;;;.;
70 | .----->7?77????7?7?COHNMNMMMMMMNMMMMN?C?CCC?7?>7>>>>>>>??7CCOO??7>>>>C>C>?NHNMNHQ?C?7-.;.-- -.;;-..;
71 |  ;.;--->7?77?7?7?O7?$QNMMMNMMMMNMMNNMM?C7CCC777?7>7>>7>77>>7?7C7?7>>7>7>77MNMNQNQOC77;;;;.;.--.-.;;.
72 | .-.;.;-;>>?7?7??7?O7OQNNNMNMMMMMMMMMMMMNO?$C7?7>C>C>7>?77>>>>>>>>>>>7>>7>ONNNNONNH777-..;..-.;;.-.;;
73 | .;;..-.-;!>?7?7C7O7CQOQMNNNMMMMMMNNMMMMMMNM?>?>C>7>>C>7?>>:>:7>?77??7?>7>$MNHQQQH$O?7;.;.;;;;-;-..;.
74 | -;.;.;-;-->>?7?77?C7OCNNNNMMMMMMHNMMMMMMMMMQC7>7>7?7>>>?!>7CCCCCC$C$C7>77HHNQ$O$QO$77.-.;.-;;.;.-.;;
75 | -..;..;;...!777?777?CHOQNNNNMMMNMONMNMMMMMMNCC>?7>>>7>7?OQ$OCC???7?7>7>77NNQHH?OCOCC>.;;.;;;-.-.-.;;
76 | ;.;...;;-;.;>?777?777$O$NHNNNMMNMCOMNMMMMMMM?C>7>>7>>>>777C????7??>7>?7>ONNOOO$OOCC?:.;;.;-.-;.;.;-.
77 | ;.;.;.;..-.;-77777?7?OOQNNNMMMNNMHCNMNMMMMMMC?7?7>>7?7>C>>77???7?7?>7>77NNNQOO$$OCC7-.-.;-.-.;-.-.;;
78 | ;.;.;-;.;.; ;...-!>77CO$NNNMNNMMMQ$QMMMMNMMNH?>777>>>>7>777777C77>7>>>7CNMNMO$HOOOO7.;.-.;-.-;.;.-;-
79 | .;..;.-;;.;..;-.;--->7OHNQMNNNMNMHQHHHMNMMMMNC>C>>C>7?>7>>>77>>>>>>>77?NMNMNNNCOCC?-;-..-.;;;;;.;;-;
80 | .;.;..-.-..;;.;;;-.->??QONQMNMNNM$NOHMNHNMMMN77>C>7>>7>7>7>>>>>7>>>>>7NNQNNHNO$OOOC.-..;.-.-;.;.-.;-
81 | .;..;.-.;.;..;;;;!;--7?O$MHNNHMNNQOMQQNHQNMNM??77?777>>7>>>>:>!>>!>>!$HNMHMCOCCCOCO..-.;.;;;;;;;.-.-
82 | ..;.;;--;..;.;;..-;.>?7?C$NNNMNNQMNHNNNNQQQMNN>C>?>777>>>>>!>>!>>>>>CO$HCC7?7???CC7.;;.;;;.;.;.;;--;
83 | .;.;.-;;;;.;..-;;;;-!!>>C>?CNNNNQQHC?C?OCOOOQN>>C>C>C>777>>>>>>7>>7>C??77?7CC7C??7>.-..;.;.;.;;.;;.-
84 | .;.;.;-;-;....;..;;;>!7!77?HNNCH$OOOC?CCO???C7C>7C>7>C7>7>7>>7>>>!7?CO>C7?O7??7?77:.;-;.; -.;.;;;---
85 | .;.;.;--;;.;.;;.;.-!-!7>$?CHHQO$O7?C?C77O?7?7777?7>C>7?7?777>7?C??7?>$!-7?7?????7>.;.-..;;.;.;.;-;;-
86 | .;.;.;-;;;-.;.;;;;-!;-!!O?OQH$?O??7?77?7?77?7777777>C>77>C7C>7>77>777O:-.??????7>...;.;;.;;.;;.;;;;-
87 | .;;;;.--;;....-.;.--.>!>??$HN???7?>?>!?7?7?77>>>7??>7C>C>7>7?7C>C7??>!-;;-7???77-;...;.-.;.;..;.----
88 | -;;;;.-;-;-.;.;;.;;.--!>?7$HN>7>>!7?7>>7?77>!:!>7>?7?7>7?7C>7?7>>7>7C---.--C7?7--..;.;;.;.; ;.;;;;--
89 | ;;;;;;;--.;..;.;;..-;-!!>?ON?!!>!!77!!!>7>!!-;!>7?>C>7?7>7>C7>7C>C7C:-.-;;-.C7-;-..;..;;.;.;.;---;;-
90 | -.;.;;.-;-;.;.;.;;.;.---!7?!:>!!!>!!!-:>!---;.:>77?>7>7>C>7>C>>>7>C:-;;;--;.;-;;.....;.-..;.;;-.----
91 | .;..;.;--.-;.;.-.;..-;-;--:-;-:-!-!!-!-7!--;..;>>7>??7C>7>C>7C7?>7!--;.-;;-;.;-.;......;..;;--;;;-;-
92 | -.;.;-.-;-;.;..-..;..;--;!;--:--;---!-!!-!;.. :>7?7?>7>7>7>C>>>7>7:-.-;-;;-;- - .. ...;.;.;.-.-;-;-;
93 | --;;.;;.--;..-.--.;.;-;;.-.;-;-;--;-:!>---.;; ;>77>?>C>C>>7>C7>>7!;-;;;;;-;...-....;..;.;.--;-;-;--;
94 | -;--;.;;;;-;. .-..;.;;;.;--.;--.;;-!-  ;-.-..;-!?>7?7>7>7?>7>?7>:-;-;;;;;- ;.;.......;.-.;;;;.-;--;-
95 | 
96 | 97 | 98 | -------------------------------------------------------------------------------- /img2txt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | 6 | (""" 7 | Usage: 8 | img2txt.py [--maxLen=] [--fontSize=] [--color] [--ansi]""" 9 | """[--bgcolor=<#RRGGBB>] [--targetAspect=] [--antialias] [--dither] 10 | img2txt.py (-h | --help) 11 | 12 | Options: 13 | -h --help show this screen. 14 | --ansi output an ANSI rendering of the image 15 | --color output a colored HTML rendering of the image. 16 | --antialias causes any resizing of the image to use antialiasing 17 | --dither dither the colors to web palette. Useful when converting 18 | images to ANSI (which has a limited color palette) 19 | --fontSize= sets font size (in pixels) when outputting HTML, 20 | default: 7 21 | --maxLen= resize image so that larger of width or height matches 22 | maxLen, default: 100px 23 | --bgcolor=<#RRGGBB> if specified, is blended with transparent pixels to 24 | produce the output. In ansi case, if no bgcolor set, a 25 | fully transparent pixel is not drawn at all, partially 26 | transparent pixels drawn as if opaque 27 | --targetAspect= resize image to this ratio of width to height. Default is 28 | 1.0 (no resize). For a typical terminal where height of a 29 | character is 2x its width, you might want to try 0.5 here 30 | """) 31 | 32 | import sys 33 | from docopt import docopt 34 | from PIL import Image 35 | import ansi 36 | from graphics_util import alpha_blend 37 | 38 | 39 | def HTMLColorToRGB(colorstring): 40 | """ convert #RRGGBB to an (R, G, B) tuple """ 41 | colorstring = colorstring.strip() 42 | if colorstring[0] == '#': 43 | colorstring = colorstring[1:] 44 | if len(colorstring) != 6: 45 | raise ValueError( 46 | "input #{0} is not in #RRGGBB format".format(colorstring)) 47 | r, g, b = colorstring[:2], colorstring[2:4], colorstring[4:] 48 | r, g, b = [int(n, 16) for n in (r, g, b)] 49 | return (r, g, b) 50 | 51 | 52 | def generate_HTML_for_image(pixels, width, height): 53 | 54 | string = "" 55 | # first go through the height, otherwise will rotate 56 | for h in range(height): 57 | for w in range(width): 58 | 59 | rgba = pixels[w, h] 60 | 61 | # TODO - could optimize output size by keeping span open until we 62 | # hit end of line or different color/alpha 63 | # Could also output just rgb (not rgba) when fully opaque - if 64 | # fully opaque is prevalent in an image 65 | # those saved characters would add up 66 | string += ("" 67 | "▇").format( 68 | rgba[0], rgba[1], rgba[2], rgba[3] / 255.0) 69 | 70 | string += "\n" 71 | 72 | return string 73 | 74 | 75 | def generate_grayscale_for_image(pixels, width, height, bgcolor): 76 | 77 | # grayscale 78 | color = "MNHQ$OC?7>!:-;. " 79 | 80 | string = "" 81 | # first go through the height, otherwise will rotate 82 | for h in range(height): 83 | for w in range(width): 84 | 85 | rgba = pixels[w, h] 86 | 87 | # If partial transparency and we have a bgcolor, combine with bg 88 | # color 89 | if rgba[3] != 255 and bgcolor is not None: 90 | rgba = alpha_blend(rgba, bgcolor) 91 | 92 | # Throw away any alpha (either because bgcolor was partially 93 | # transparent or had no bg color) 94 | # Could make a case to choose character to draw based on alpha but 95 | # not going to do that now... 96 | rgb = rgba[:3] 97 | 98 | string += color[int(sum(rgb) / 3.0 / 256.0 * 16)] 99 | 100 | string += "\n" 101 | 102 | return string 103 | 104 | 105 | def load_and_resize_image(imgname, antialias, maxLen, aspectRatio): 106 | 107 | if aspectRatio is None: 108 | aspectRatio = 1.0 109 | 110 | img = Image.open(imgname) 111 | 112 | # force image to RGBA - deals with palettized images (e.g. gif) etc. 113 | if img.mode != 'RGBA': 114 | img = img.convert('RGBA') 115 | 116 | # need to change the size of the image? 117 | if maxLen is not None or aspectRatio != 1.0: 118 | 119 | native_width, native_height = img.size 120 | 121 | new_width = native_width 122 | new_height = native_height 123 | 124 | # First apply aspect ratio change (if any) - just need to adjust one axis 125 | # so we'll do the height. 126 | if aspectRatio != 1.0: 127 | new_height = int(float(aspectRatio) * new_height) 128 | 129 | # Now isotropically resize up or down (preserving aspect ratio) such that 130 | # longer side of image is maxLen 131 | if maxLen is not None: 132 | rate = float(maxLen) / max(new_width, new_height) 133 | new_width = int(rate * new_width) 134 | new_height = int(rate * new_height) 135 | 136 | if native_width != new_width or native_height != new_height: 137 | img = img.resize((new_width, new_height), Image.ANTIALIAS if antialias else Image.NEAREST) 138 | 139 | return img 140 | 141 | 142 | def floydsteinberg_dither_to_web_palette(img): 143 | 144 | # Note that alpha channel is thrown away - if you want to keep it you need to deal with it yourself 145 | # 146 | # Here's how it works: 147 | # 1. Convert to RGB if needed - we can't go directly from RGBA because Image.convert will not dither in this case 148 | # 2. Convert to P(alette) mode - this lets us kick in dithering. 149 | # 3. Convert back to RGBA, which is where we want to be 150 | # 151 | # Admittedly converting back and forth requires more memory than just dithering directly 152 | # in RGBA but that's how the library works and it isn't worth writing it ourselves 153 | # or looking for an alternative given current perf needs. 154 | 155 | if img.mode != 'RGB': 156 | img = img.convert('RGB') 157 | img = img.convert(mode="P", matrix=None, dither=Image.FLOYDSTEINBERG, palette=Image.WEB, colors=256) 158 | img = img.convert('RGBA') 159 | return img 160 | 161 | 162 | def dither_image_to_web_palette(img, bgcolor): 163 | 164 | if bgcolor is not None: 165 | # We know the background color so flatten the image and bg color together, thus getting rid of alpha 166 | # This is important because as discussed below, dithering alpha doesn't work correctly. 167 | img = Image.alpha_composite(Image.new("RGBA", img.size, bgcolor), img) # alpha blend onto image filled with bgcolor 168 | dithered_img = floydsteinberg_dither_to_web_palette(img) 169 | else: 170 | 171 | """ 172 | It is not possible to correctly dither in the presence of transparency without knowing the background 173 | that the image will be composed onto. This is because dithering works by propagating error that is introduced 174 | when we select _available_ colors that don't match the _desired_ colors. Without knowing the final color value 175 | for a pixel, it is not possible to compute the error that must be propagated FROM it. If a pixel is fully or 176 | partially transparent, we must know the background to determine the final color value. We can't even record 177 | the incoming error for the pixel, and then later when/if we know the background compute the full error and 178 | propagate that, because that error needs to propagate into the original color selection decisions for the other 179 | pixels. Those decisions absorb error and are lossy. You can't later apply more error on top of those color 180 | decisions and necessarily get the same results as applying that error INTO those decisions in the first place. 181 | 182 | So having established that we could only handle transparency correctly at final draw-time, shouldn't we just 183 | dither there instead of here? Well, if we don't know the background color here we don't know it there either. 184 | So we can either not dither at all if we don't know the bg color, or make some approximation. We've chosen 185 | the latter. We'll handle it here to make the drawing code simpler. So what is our approximation? We basically 186 | just ignore any color changes dithering makes to pixels that have transparency, and prevent any error from being 187 | propagated from those pixels. This is done by setting them all to black before dithering (using an exact-match 188 | color in Floyd Steinberg dithering with a web-safe-palette will never cause a pixel to receive enough inbound error 189 | to change color and thus will not propagate error), and then afterwards we set them back to their original values. 190 | This means that transparent pixels are essentially not dithered - they ignore (and absorb) inbound error but they 191 | keep their original colors. We could alternately play games with the alpha channel to try to propagate the error 192 | values for transparent pixels through to when we do final drawing but it only works in certain cases and just isn't 193 | worth the effort (which involves writing the dithering code ourselves for one thing). 194 | """ 195 | 196 | # Force image to RGBA if it isn't already - simplifies the rest of the code 197 | if img.mode != 'RGBA': 198 | img = img.convert('RGBA') 199 | 200 | rgb_img = img.convert('RGB') 201 | 202 | orig_pixels = img.load() 203 | rgb_pixels = rgb_img.load() 204 | width, height = img.size 205 | 206 | for h in range(height): # set transparent pixels to black 207 | for w in range(width): 208 | if (orig_pixels[w, h])[3] != 255: 209 | rgb_pixels[w, h] = (0, 0, 0) # bashing in a new value changes it! 210 | 211 | dithered_img = floydsteinberg_dither_to_web_palette(rgb_img) 212 | 213 | dithered_pixels = dithered_img.load() # must do it again 214 | 215 | for h in range(height): # restore original RGBA for transparent pixels 216 | for w in range(width): 217 | if (orig_pixels[w, h])[3] != 255: 218 | dithered_pixels[w, h] = orig_pixels[w, h] # bashing in a new value changes it! 219 | 220 | return dithered_img 221 | 222 | 223 | 224 | if __name__ == '__main__': 225 | 226 | dct = docopt(__doc__) 227 | 228 | imgname = dct[''] 229 | 230 | maxLen = dct['--maxLen'] 231 | 232 | clr = dct['--color'] 233 | 234 | do_ansi = dct['--ansi'] 235 | 236 | fontSize = dct['--fontSize'] 237 | 238 | bgcolor = dct['--bgcolor'] 239 | 240 | antialias = dct['--antialias'] 241 | 242 | dither = dct['--dither'] 243 | 244 | target_aspect_ratio = dct['--targetAspect'] 245 | 246 | try: 247 | maxLen = float(maxLen) 248 | except: 249 | maxLen = 100.0 # default maxlen: 100px 250 | 251 | try: 252 | fontSize = int(fontSize) 253 | except: 254 | fontSize = 7 255 | 256 | try: 257 | # add fully opaque alpha value (255) 258 | bgcolor = HTMLColorToRGB(bgcolor) + (255, ) 259 | except: 260 | bgcolor = None 261 | 262 | try: 263 | target_aspect_ratio = float(target_aspect_ratio) 264 | except: 265 | target_aspect_ratio = 1.0 # default target_aspect_ratio: 1.0 266 | 267 | try: 268 | img = load_and_resize_image(imgname, antialias, maxLen, target_aspect_ratio) 269 | except IOError: 270 | exit("File not found: " + imgname) 271 | 272 | # Dither _after_ resizing 273 | if dither: 274 | img = dither_image_to_web_palette(img, bgcolor) 275 | 276 | # get pixels 277 | pixel = img.load() 278 | 279 | width, height = img.size 280 | 281 | if do_ansi: 282 | 283 | # Since the "current line" was not established by us, it has been 284 | # filled with the current background color in the 285 | # terminal. We have no ability to read the current background color 286 | # so we want to refill the line with either 287 | # the specified bg color or if none specified, the default bg color. 288 | if bgcolor is not None: 289 | # Note that we are making the assumption that the viewing terminal 290 | # supports BCE (Background Color Erase) otherwise we're going to 291 | # get the default bg color regardless. If a terminal doesn't 292 | # support BCE you can output spaces but you'd need to know how many 293 | # to output (too many and you get linewrap) 294 | fill_string = ansi.getANSIbgstring_for_ANSIcolor( 295 | ansi.getANSIcolor_for_rgb(bgcolor)) 296 | else: 297 | # reset bg to default (if we want to support terminals that can't 298 | # handle this will need to instead use 0m which clears fg too and 299 | # then when using this reset prior_fg_color to None too 300 | fill_string = "\x1b[49m" 301 | fill_string += "\x1b[K" # does not move the cursor 302 | sys.stdout.write(fill_string) 303 | 304 | sys.stdout.write( 305 | ansi.generate_ANSI_from_pixels(pixel, width, height, bgcolor)[0]) 306 | 307 | # Undo residual color changes, output newline because 308 | # generate_ANSI_from_pixels does not do so 309 | # removes all attributes (formatting and colors) 310 | sys.stdout.write("\x1b[0m\n") 311 | else: 312 | 313 | if clr: 314 | # TODO - should handle bgcolor - probably by setting it as BG on 315 | # the CSS for the pre 316 | string = generate_HTML_for_image(pixel, width, height) 317 | else: 318 | string = generate_grayscale_for_image( 319 | pixel, width, height, bgcolor) 320 | 321 | # wrap with html 322 | 323 | template = """ 324 | 325 | 326 | 327 | 339 | 340 | 341 |
%s
342 | 343 | 344 | """ 345 | 346 | html = template % (fontSize, string) 347 | sys.stdout.write(html) 348 | 349 | 350 | sys.stdout.flush() 351 | -------------------------------------------------------------------------------- /ansi.py: -------------------------------------------------------------------------------- 1 | from graphics_util import alpha_blend 2 | 3 | 4 | def getANSIcolor_for_rgb(rgb): 5 | # Convert to web-safe color since that's what terminals can handle in "256 color mode" 6 | # https://en.wikipedia.org/wiki/ANSI_escape_code 7 | # http://misc.flogisoft.com/bash/tip_colors_and_formatting#bash_tipscolors_and_formatting_ansivt100_control_sequences 8 | # http://superuser.com/questions/270214/how-can-i-change-the-colors-of-my-xterm-using-ansi-escape-sequences 9 | websafe_r = int(round((rgb[0] / 255.0) * 5) ) 10 | websafe_g = int(round((rgb[1] / 255.0) * 5) ) 11 | websafe_b = int(round((rgb[2] / 255.0) * 5) ) 12 | 13 | # Return ANSI color - only using 216 colors since those are the only ones we can reliably map to 14 | # https://en.wikipedia.org/wiki/ANSI_escape_code (see 256 color mode section) 15 | return int(((websafe_r * 36) + (websafe_g * 6) + websafe_b) + 16) 16 | 17 | 18 | def getANSIfgarray_for_ANSIcolor(ANSIcolor): 19 | "Return array of color codes to be used in composing an SGR escape sequence. Using array form lets us compose multiple color updates without putting out additional escapes" 20 | # We are using "256 color mode" which is available in xterm but not necessarily all terminals 21 | return ['38', '5', str(ANSIcolor)] # To set FG in 256 color you use a code like ESC[38;5;###m 22 | 23 | 24 | def getANSIbgarray_for_ANSIcolor(ANSIcolor): 25 | "Return array of color codes to be used in composing an SGR escape sequence. Using array form lets us compose multiple color updates without putting out additional escapes" 26 | # We are using "256 color mode" which is available in xterm but not necessarily all terminals 27 | return ['48', '5', str(ANSIcolor)] # To set BG in 256 color you use a code like ESC[48;5;###m 28 | 29 | 30 | def getANSIbgstring_for_ANSIcolor(ANSIcolor): 31 | # Get the array of color code info, prefix it with ESCAPE code and terminate it with "m" 32 | return "\x1b[" + ";".join(getANSIbgarray_for_ANSIcolor(ANSIcolor)) + "m" 33 | 34 | 35 | def generate_ANSI_to_set_fg_bg_colors(cur_fg_color, cur_bg_color, new_fg_color, new_bg_color): 36 | 37 | # This code assumes that ESC[49m and ESC[39m work for resetting bg and fg 38 | # This may not work on all terminals in which case we would have to use ESC[0m 39 | # to reset both at once, and then put back fg or bg that we actually want 40 | 41 | # We don't change colors that are already the way we want them - saves lots of file size 42 | 43 | color_array = [] # use array mechanism to avoid multiple escape sequences if we need to change fg AND bg 44 | 45 | if new_bg_color != cur_bg_color: 46 | if new_bg_color is None: 47 | color_array.append('49') # reset to default 48 | else: 49 | color_array += getANSIbgarray_for_ANSIcolor(new_bg_color) 50 | 51 | if new_fg_color != cur_fg_color: 52 | if new_fg_color is None: 53 | color_array.append('39') # reset to default 54 | else: 55 | color_array += getANSIfgarray_for_ANSIcolor(new_fg_color) 56 | 57 | if len(color_array) > 0: 58 | return "\x1b[" + ";".join(color_array) + "m" 59 | else: 60 | return "" 61 | 62 | 63 | def generate_optimized_y_move_down_x_SOL(y_dist): 64 | """ move down y_dist, set x=0 """ 65 | 66 | # Optimization to move N lines and go to SOL in one command. Note that some terminals 67 | # may not support this so we might have to remove this optimization or make it optional 68 | # if that winds up mattering for terminals we care about. If we had to remove we'd 69 | # want to rework things such that we used "\x1b[{0}B" but also we would want to change 70 | # our interface to this function so we didn't guarantee x=0 since caller might ultimate 71 | # want it in a different place and we don't want to output two x moves. Could pass in 72 | # desired x, or return current x from here. 73 | 74 | string = "\x1b[{0}E".format(y_dist) # ANSI code to move down N lines and move x to SOL 75 | 76 | # Would a sequence of 1 or more \n chars be cheaper? If so we'll output that instead 77 | if y_dist < len(string): 78 | string = '\n' * y_dist 79 | 80 | return string 81 | 82 | 83 | def generate_ANSI_to_move_cursor(cur_x, cur_y, target_x, target_y): 84 | """ 85 | Note that x positions are absolute (0=SOL) while y positions are relative. That is, 86 | we move the y position the relative distance between cur_y and target_y. It doesn't 87 | mean that cur_y=0 means we are on the first line of the screen. We have no way of 88 | knowing how tall the screen is, etc. at draw-time so we can't know this. 89 | """ 90 | 91 | 92 | """ 93 | **SIZE - this code (in concert with its caller) implements what I would call "local optimizations" 94 | to try to minimize the number and size of cursor movements outputted. It does not attempt "global 95 | optimizations" which I think are rarely going to be worthwhile. See the DESIGN NOTE on global 96 | optimizations in this file for more details 97 | """ 98 | 99 | 100 | string = "" 101 | 102 | if cur_y < target_y: # MOVE DOWN 103 | y_dist = target_y - cur_y 104 | 105 | # See if we can optimize moving x and y together 106 | if cur_x == target_x: 107 | 108 | # Need to move in y only 109 | if target_x != 0: 110 | # Already in correct x position which is NOT SOL. Just output code to move cursor 111 | # down. No special optimization is possible since \n would take us to SOL and then 112 | # we'd also need to output a move for x. 113 | return "\x1b[{0}B".format(y_dist) # ANSI code to move down N lines 114 | else: 115 | # Already in correct x position which is SOL. Output efficient code to move down. 116 | return generate_optimized_y_move_down_x_SOL(y_dist) 117 | else: 118 | 119 | # Need to move in x and y 120 | if target_x != 0: 121 | # x move is going to be required so we'll move y efficiently and as a side 122 | # effect, x will become 0. Code below will move x to the right place 123 | string += generate_optimized_y_move_down_x_SOL(y_dist) 124 | cur_x = 0 125 | else: 126 | # Output move down that brings x to SOL. Then we're done. 127 | return generate_optimized_y_move_down_x_SOL(y_dist) 128 | 129 | elif cur_y > target_y: # MOVE UP 130 | if target_x == 0: 131 | # We want to move up and be at the SOL. That can be achieved with one command so we're 132 | # done and we return it. However note that some terminals may not support this so we 133 | # might have to remove this optimization or make it optional if that winds up mattering for terminals we care about. 134 | return "\x1b[{0}F".format(cur_y - target_y) # ANSI code to move up N lines and move x to SOL 135 | else: 136 | string += "\x1b[{0}A".format(cur_y - target_y) # ANSI code to move up N lines 137 | 138 | if cur_x < target_x: # MOVE RIGHT 139 | # **SIZE - Note that when the bgcolor is specified (not None) and not overdrawing another drawing (as in an animation case) 140 | # an optimization could be performed to draw spaces rather than output cursor advances. This would use less 141 | # size when advancing less than 3 columns since the min escape sequence here is len 4. Not implementing this now 142 | # \t (tab) could also be a cheap way to move forward, but not clear we can determine how far it goes or if that would 143 | # be consistent, nor whether it is ever destructive. 144 | string += "\x1b[{0}C".format(target_x - cur_x) # ANSI code to move cursor right N columns 145 | elif cur_x > target_x: # MOVE LEFT 146 | # **SIZE - potential optimizations: \b (backspace) could be a cheaper way to move backwards when there is only a short 147 | # way to go. However, not sure if it is ever destructive so not bothering with it now. 148 | # If we need to move to x=0, \r could be a cheap way to get there. However not entirely clear whether some terminals 149 | # will move to next line as well, and might sometimes be destructive. Not going to research this so not doing it now. 150 | string += "\x1b[{0}D".format(cur_x - target_x) # ANSI code to move cursor left N columns 151 | 152 | return string 153 | 154 | 155 | def generate_ANSI_from_pixels(pixels, width, height, bgcolor_rgba, current_ansi_colors = None, current_cursor_pos = None, get_pixel_func = None, is_overdraw = False, x_offset = 0): 156 | """ 157 | Generate ANSI codes for passed pixels 158 | 159 | Does not include a final newline or a reset to any particular colors at end of returned output string. 160 | Caller should take care of that if desired. 161 | 162 | :param pixels: if get_pixel_func is None, 2D array of RGBA tuples indexed by [x,y]. 163 | Otherwise given to get_pixel_func as param. 164 | :param width: number of pixels to output on each row 165 | :param height: number of rows to output 166 | :param bgcolor_rgba: Optional background color used to fill new lines (produced when is_ovedraw is False) 167 | and a net new line to the terminal (as opposed to drawing on a current line - e.g. if the cursor was moved 168 | up) is produced. Also used as background color for any characters we output that don't fill the entire 169 | character area (e.g. a space fills the entire area, while X does not). Non-space only used if get_pixel_func 170 | returns it. If bgcolor_rgba is None, then the background is treated as the terminal's default background color 171 | which also means that partially transparent pixels will be treated as non-transparent (since we don't know 172 | bg color to blend them with). 173 | :param current_ansi_colors: Optional dict holding "current" ANSI colors - allows optimization where 174 | we don't switch to these colors if already set. See info on return values for format of dict. 175 | :param current_cursor_pos: Optional dict holding current cursor position - allows optimization where 176 | we don't output extra moves to get to the right place to draw. Consider the passed position relative 177 | to where we want to draw the top/left for the current call. Note that a negative value for 178 | current_cursor_pos['y'] can be used to start drawing futher down the screen. Don't use ['x'] similarly 179 | since x is reset for each line. Use the x_offset param instead. 180 | :param get_pixel_func: Optional function that allows using custom "pixel" formats. If not None, function 181 | that will be passed pixels and a current x,y value and must return character to draw and RGBA to draw it in. 182 | :param is_overdraw: if True, drawing code can assume that all lines are being drawn on lines that were already 183 | established in the terminal. This allows for optimizations (e.g. not needing to output \n to fill blank lines). 184 | :param x_offset: If not zero, allows drawing each line starting at a particular X offset. Useful if 185 | you don't want it drawn at x=0. Must be >=0 186 | 187 | Returns tuple: 188 | string containing ANSI codes 189 | dict of form {'fg': (r,g,b,a), 'bg': (r,g,b,a)} holding current fg/bg color - suitable for passing as current_ansi_colors param 190 | dict of form {'x': , 'y': } holding final x,y cursor positions - x is absolute since \n sends it to 0. y is relative to incoming y (or 0 if none). Suitable for passing as current_cursor_pos param 191 | """ 192 | 193 | if get_pixel_func is None: 194 | get_pixel_func = lambda pixels, x, y: (" ", pixels[x, y]) # just treat pixels as 2D array 195 | 196 | # Compute ANSI bg color and strings we'll use to reset colors when moving to next line 197 | if bgcolor_rgba is not None: 198 | bgcolor_ANSI = getANSIcolor_for_rgb(bgcolor_rgba) 199 | # Reset cur bg color to bgcolor because \n will fill the new line with this color 200 | bgcolor_ANSI_string = getANSIbgstring_for_ANSIcolor(bgcolor_ANSI) 201 | else: 202 | bgcolor_ANSI = None 203 | # Reset cur bg color default because \n will fill the new line with this color (possibly only if BCE supported by terminal) 204 | bgcolor_ANSI_string = "\x1b[49m" # reset bg to default (if we want to support terminals that can't handle this will need to instead use 0m which clears fg too and then when using this reset prior_fg_color to None too 205 | 206 | # Do we know the current ANSI colors that have been set? 207 | if current_ansi_colors is not None: 208 | string = "" 209 | prior_fg_color = current_ansi_colors['fg'] # Value of None is OK - means default 210 | prior_bg_color = current_ansi_colors['bg'] # Value of None is OK - means default 211 | else: 212 | # We don't know the current colors so output a reset to terminal defaults - we want to be in a known state 213 | # **SIZE - could suppress outputting this here, and remember that we have unknown (not same as default) 214 | # colors. Then when we need to output we can take this into account. If we wind up setting both fg and bg colors 215 | # for output (as for a non-space) then we'd never need to output the reset. 216 | # I'm not going to implement this now since the better thing to do for repeated calls is to pass current_ansi_colors 217 | # so we'd never get to this case. 218 | string = "\x1b[0m" # removes all attributes (formatting and colors) to start in a known state 219 | prior_fg_color = None # this is an ANSI color not rgba. None means default. 220 | prior_bg_color = None # this is an ANSI color not rgba. None means default. 221 | 222 | # Do we know the cursor pos? 223 | if current_cursor_pos is not None: 224 | cursor_x = current_cursor_pos['x'] 225 | cursor_y = current_cursor_pos['y'] 226 | else: 227 | cursor_x = 0 228 | cursor_y = 0 229 | 230 | for h in range(height): 231 | for w in range(width): 232 | 233 | draw_char, rgba = get_pixel_func(pixels, w, h) 234 | 235 | # Handle fully or partially transparent pixels - but not if it is the special "erase" character (None) 236 | skip_pixel = False 237 | if draw_char is not None: 238 | alpha = rgba[3] 239 | if alpha == 0: 240 | skip_pixel = True # skip any full transparent pixel. Note that we don't output a bgcolor space (in specified or default cases). Why? In overdraw mode, that would be wrong since whatever is already drawn should show through. In non-overdraw, assumption is that any line we're drawing on has already been filled with bgcolor so lets not do extra output. If this was an issue in practice, could make it an option. 241 | elif alpha != 255 and bgcolor_rgba is not None: 242 | rgba = alpha_blend(rgba, bgcolor_rgba) # non-opaque so blend with specified bgcolor 243 | 244 | if not skip_pixel: 245 | 246 | this_pixel_str = "" 247 | 248 | # Throw away alpha channel - can still have non-fully-opaque alpha value here if 249 | # bgcolor was partially transparent or if no bgcolor and not fully transparent 250 | # Could make argument to use threshold to decide if throw away (e.g. >50% transparent) 251 | # vs. consider opaque (e.g. <50% transparent) but at least for now we just throw it away 252 | # which means we treat the pixel as fully opaque. 253 | rgb = rgba[:3] 254 | 255 | # If we've got the special "erase" character turn it into outputting a space using the bgcolor 256 | # which if None will just be a reset to default bg which is what we want 257 | if draw_char is None: 258 | draw_char = " " 259 | color = bgcolor_ANSI 260 | else: 261 | # Convert from RGB to ansi color, using closest color. Conceivably we could optionally support 262 | # dithering to spread the color error. Problematic when dealing with transparency (see cmt in dither_image_to_web_palette()) 263 | # or unknown/default bgcolor, and currently not worthwhile since either easy (img2txt) or more correct (graphics) to do 264 | # dithering upstream. 265 | color = getANSIcolor_for_rgb(rgb) 266 | 267 | # Optimization - if we're drawing a space and the color is the same as a specified bg color 268 | # then just skip this pixel. We need to make this check here because the conversion to ANSI above can 269 | # cause colors that didn't match to now match 270 | # We cannot do this optimization in overdraw mode because we cannot assume that the bg color 271 | # is already drawn at this location. We could presumably pass in the known state of the screen 272 | # and thus have this knoweldge if the optimization was worthwhile. 273 | if not is_overdraw and (draw_char == " ") and (color == bgcolor_ANSI): 274 | skip_pixel = True 275 | 276 | if not skip_pixel: 277 | 278 | if len(draw_char) > 1: 279 | raise ValueError("Not allowing multicharacter draw strings") 280 | 281 | # If we are not at the cursor location where we need to draw (happens if we skip pixels or lines) 282 | # then output ANSI sequence to move cursor there. 283 | # This is how we implement transparency - we don't draw spaces, we skip via cursor moves 284 | # We take the x_offset (if any) into account here 285 | ofsetted_w = x_offset + w 286 | if (cursor_x != ofsetted_w) or (cursor_y != h): 287 | string += generate_ANSI_to_move_cursor(cursor_x, cursor_y, ofsetted_w, h) 288 | cursor_x = ofsetted_w 289 | cursor_y = h 290 | 291 | # Generate the ANSI sequences to set the colors the way we want them 292 | if draw_char == " ": 293 | 294 | # **SIZE - If we are willing to assume terminals that support ECH (Erase Character) as specified 295 | # in here http://vt100.net/docs/vt220-rm/chapter4.html we could replace long runs of same-color 296 | # spaces with single ECH codes. Seems like it is only correct to do this if BCE is supported 297 | # though (http://superuser.com/questions/249898/how-can-i-prevent-os-x-terminal-app-from-overriding-vim-colours-on-a-remote-syst) 298 | # else "erase" would draw the _default_ background color not the currently set background color 299 | # Note that if we implement this by accumulating spaces (as opposed to lookahead), need to output that 300 | # before any different output be that a color change, or if we need to output a \n (if line ended 301 | # in same-color spaces in non-overdraw) 302 | 303 | # We are supposed to output a space, so we're going to need to change the background color. 304 | # No, we can't output an "upper ascii" character that fills the entire foreground - all terminals 305 | # don't display such characters the same way, if at all. e.g. Mac terminal outputs ? for "upper ascii" chars 306 | # Since we're outputting a space we can leave the prior fg color intact as it won't be used 307 | string += generate_ANSI_to_set_fg_bg_colors(prior_fg_color, prior_bg_color, prior_fg_color, color) 308 | prior_bg_color = color 309 | 310 | else: 311 | # We're supposed to output a non-space character, so we're going to need to change the foreground color 312 | # and make sure the bg is set appropriately 313 | string += generate_ANSI_to_set_fg_bg_colors(prior_fg_color, prior_bg_color, color, bgcolor_ANSI) 314 | prior_fg_color = color 315 | prior_bg_color = bgcolor_ANSI 316 | 317 | # Actually output the character 318 | string += draw_char 319 | 320 | cursor_x = cursor_x + 1 321 | 322 | # Handle end of line - unless last line which is NOP because we don't want to do anything to the _line after_ our drawing 323 | # and outputting \n would establish it and fill it 324 | if (h + 1) != height: 325 | 326 | # Move to next line. If this establishes a new line in the terminal then it fills the _newly established line_ 327 | # up to EOL with current bg color. Filling with the current bg color vs. default might be dependent on terminal's 328 | # support for BCE (Background Color Erase) - I'm not sure. 329 | # If cursor had been moved up and this just goes back down to an existing line, no filling occurs 330 | # In overdraw mode, we are going to assume we don't need to establish/fill a new line (which could be untrue 331 | # if we are overdrawing some lines but going further down too - if that becomes important can allow passing 332 | # in how many lines we can go down before hitting that). Next time we need to draw in overdraw mode we'll 333 | # move the cursor down as needed. 334 | if not is_overdraw: 335 | 336 | # If not already desired color, reset bg color so \n fills with it 337 | # NOTE: it would be ideal to optionally dither the background color if it is not perfectly resolvable 338 | # in the palette we have to work with. However, we can't actually do this in the general case because 339 | # we don't know the width of the terminal (which can be different at display-time) and because we 340 | # don't always know the bg color ("default" is not known by us, and not known by anybody until display-time) 341 | if prior_bg_color != bgcolor_ANSI: 342 | string += bgcolor_ANSI_string; 343 | prior_bg_color = bgcolor_ANSI 344 | 345 | # If the cursor is not at the correct y, move it there before outputting the newline 346 | # In current use this will only occur if current_cursor_pos includes a y offset and 347 | # the first line was entirely transparent. We pass 0/0 for cur/target x because no need 348 | # to adjust x as it will be changed by the \n 349 | if (cursor_y != h): 350 | string += generate_ANSI_to_move_cursor(0, cursor_y, 0, h) 351 | cursor_y = h 352 | 353 | string += "\n" 354 | cursor_y += 1 355 | cursor_x = 0 # we are assuming UNIX-style \n behavior - if it were windows we'd have to output \r to get cursor_x to 0 356 | 357 | return string, {'fg': prior_fg_color, 'bg': prior_bg_color}, { 'x': cursor_x, 'y': cursor_y } 358 | 359 | 360 | """ 361 | DESIGN NOTE (Global Optimization) 362 | 363 | The code in this file currently implements "local optimization" to minimize the cost of moving 364 | the cursor around and changing colors. However, it always follows a top-to-bottom left-to-right 365 | path. There are scenarios where choosing a different path would yield a more optimal result 366 | (smaller output size). I have not bothered to implement any global optimization because I 367 | think it will rarely produce a better output. 368 | 369 | Here's an example of a scenario where a global optimization of cursor movements that didn't just 370 | go scanline by scanline top to bottom left to right would be a win: 371 | 372 | For example, assume this pattern is to be drawn, beginning at x=0 (SOL) 373 | XXX XXX 374 | XXX XXX 375 | XXX XXX 376 | Drawing it top down/left to right we must do 13 operations: 377 | XXX, move right, XXX, \n, move right, XXX, move right, XXX, \n, move right, XXX, move right, XXX 378 | Drawing it in an optimal sequence we can do 11 operations: 379 | XXX, move down, XXX, move down, XXX, move up, XXX, move down, XXX, move down, XXX 380 | However, since \n is cheaper than move down, we actually would need blank lines between the XXX lines 381 | to really make the second case smaller in terms of bytes (vs operations). 382 | 383 | The discussion above covers cursor changes, but of cours color changes play a role as well. If we were 384 | to assume the XXX on the left were one color while the XXX on the right were another, we'd also save four 385 | color change operations. 386 | 387 | To perfectly implement global optimization, you essentially need to solve a variant of the Traveling 388 | Salesman Path Problem (TSPP) as I discuss here: http://stackoverflow.com/questions/20032768/graph-traversal-with-shortest-path-with-any-source/33601043#33601043 389 | We could use the single fixed endpoint variant (P*s) from the Hoogeveen paper. Note that each character 390 | we want to output is essentially a node in the graph, and the graph is fully connected (can move from 391 | any character to any other via cursor moves, changing color as needed). Some edges are free (moving right 392 | while outputting character of same color). It is actually an asymmetric TSP because there are cases 393 | where e.g. moving right is free and moving left is not, and moving down to SOL via \n costs 1 while moving 394 | back up to the x pos could be several bytes. Can solve asymmetric TSP via conversion to symmetric. 395 | Solving a TSPP is generally computationally infeasible, so approximation algorithms such as Hoogeveen's are used. 396 | Hoogeveen run O(n^3) so it too may be too slow. Can reduce n by combining runs of same color - I haven't bothered 397 | to prove it but I believe that this does not harm the optimality of the result. Note that this does not reduce 398 | the worst case n - you can a case where there are no such runs. I believe that there are faster algorithms 399 | that provide worse (or zero) optimality guarantees - e.g. Lin Kernighan or nearest neighbor. These might be geared 400 | to solve TSP vs TSPP - though a solution to TSP is also a solution to TSPP, just with the cycle completed and 401 | no prescribed starting location. We would remove the cycle completing hop, and output a move to the chosen start 402 | location as needed. The algorithms might also be adaptable to TSPP directly. 403 | If TSPP solvers can never be made fast enough, heuristics can likely be employed to good effect. 404 | Solutions from a TSPP solver might be a good way to find such heuristics. 405 | 406 | ANSI codes to save/restore cursor pos could open new vistas of global optimization since you can 407 | restore x/y in only 3 bytes but they are seemingly not supported in Mac xterm so I don't use them. 408 | """ 409 | --------------------------------------------------------------------------------