From 692243774725c972d99931e7681f8c76ed81b982 Mon Sep 17 00:00:00 2001 From: acidvegas Date: Mon, 26 Jun 2023 02:41:28 -0400 Subject: [PATCH] Added .ascii img command to image to IRC art conversion --- README.md | 29 +++++++------- img2irc.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ scroll.py | 23 ++++++++--- 3 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 img2irc.py diff --git a/README.md b/README.md index 13687e5..40b93d0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ There is no API key needed, no local art files needed, & no reason to not setup ## Dependencies * [python](https://www.python.org/) * [chardet](https://pypi.org/project/chardet/) *(`pip install chardet`)* +* [pillow](https://pypi.org/project/pillow/) *(`pip install pillow`)* ## Commands | Command | Description | @@ -16,6 +17,7 @@ There is no API key needed, no local art files needed, & no reason to not setup | `@scroll` | information about scroll | | `.ascii ` | play the \ art file | | `.ascii dirs` | list of art directories | +| `.ascii img ` | convert image \ to art | | `.ascii list` | list of art filenames | | `.ascii play ` | play the contents of \ *(must be a raw pastebin url)* | | `.ascii random [dir]` | play random art, optionally from the [dir] directory only | @@ -29,14 +31,16 @@ There is no API key needed, no local art files needed, & no reason to not setup **NOTE**: The sync & settings commands are admin only! `admin` is a *nick!user@host* mask defined in [scroll.py](https://github.com/ircart/scroll/blob/master/scroll.py) ## Settings -| Setting | Description | -| --------- | ---------------------------------------------------------------------------- | -| `flood` | delay between each command | -| `ignore` | directories to ignore in `.ascii random` *(comma seperated list, no spaces)* | -| `lines` | max lines outside of #scroll | -| `msg` | delay between each message sent | -| `results` | max results to return in `.ascii search` | -| `paste` | enable or disable `.ascii play` | +| Setting | Description | +| ----------- | ---------------------------------------------------------------------------- | +| `flood` | delay between each command | +| `ignore` | directories to ignore in `.ascii random` *(comma seperated list, no spaces)* | +| `lines` | max lines outside of #scroll | +| `msg` | delay between each message sent | +| `paste` | enable or disable `.ascii play` | +| `png_width` | maximum width for `.ascii img` output | +| `results` | max results to return in `.ascii search` | + ## Preview @@ -44,8 +48,7 @@ There is no API key needed, no local art files needed, & no reason to not setup Come pump with us in **#scroll** on [irc.supernets.org](ircs://irc.supernets.org) -## Mirrors -- [acid.vegas](https://git.acid.vegas/scroll) -- [GitHub](https://github.com/ircart/scroll) -- [GitLab](https://gitlab.com/ircart/scroll) -- [SuperNETs](https://git.supernets.org/ircart/scroll) \ No newline at end of file +___ + +###### Mirrors +[acid.vegas](https://git.acid.vegas/asciiblaster) • [GitHub](https://github.com/ircart/asciiblaster) • [GitLab](https://gitlab.com/ircart/asciiblaster) • [SourceHut](https://git.sr.ht/~acidvegas/asciiblaster) • [SuperNETs](https://git.supernets.org/ircart/asciiblaster) diff --git a/img2irc.py b/img2irc.py new file mode 100644 index 0000000..8d79719 --- /dev/null +++ b/img2irc.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# Scroll IRC Art Bot - Developed by acidvegas in Python (https://git.acid.vegas/scroll) + +'''big props to wrk (wr34k) for this one, did most of the work, just cleaned it up a little''' + +import io + +try: + from PIL import Image +except ImportError: + raise SystemExit('missing required \'pillow\' library (https://pypi.org/project/pillow/)') + +async def convert(data, img_width=80): + image = Image.open(io.BytesIO(data)) + del data + (width, height) = image.size + img_height = img_width / width * height + del height, width + image.thumbnail((img_width, img_height), Image.Resampling.LANCZOS) + del img_height, img_width + ansi_image = AnsiImage(image) + del image + CHAR = '\u2580' + buf = '' + for (y, row) in enumerate(ansi_image.halfblocks): + last_fg = -1 + last_bg = -1 + for (x, pixel_pair) in enumerate(row): + fg = pixel_pair.top.irc + bg = pixel_pair.bottom.irc + if x != 0: + if fg == last_fg and bg == last_bg: + buf += CHAR + elif bg == last_bg: + buf += f'\x03{fg}{CHAR}' + else: + buf += f'\x03{fg},{bg}{CHAR}' + else: + buf += f'\x03{fg},{bg}{CHAR}' + last_fg = fg + last_bg = bg + if y != len(ansi_image.halfblocks) - 1: + buf += '\n' + else: + buf += '\x0f' + return buf.splitlines() + +def hex_to_rgb(color): + r = color >> 16 + g = (color >> 8) % 256 + b = color % 256 + return (r,g,b) + +def rgb_to_hex(rgb): + (r,g,b) = rgb + return (r << 16) + (g << 8) + b + +def color_distance_squared(c1, c2): + dr = c1[0] - c2[0] + dg = c1[1] - c2[1] + db = c1[2] - c2[2] + return dr * dr + dg * dg + db * db + +class AnsiPixel: + def __init__(self, pixel_u32): + self.RGB99 = [ + 0xffffff, 0x000000, 0x00007f, 0x009300, 0xff0000, 0x7f0000, 0x9c009c, 0xfc7f00, + 0xffff00, 0x00fc00, 0x009393, 0x00ffff, 0x0000fc, 0xff00ff, 0x7f7f7f, 0xd2d2d2, + 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, + 0x000047, 0x2e0047, 0x470047, 0x47002a, 0x740000, 0x743a00, 0x747400, 0x517400, + 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045, + 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, + 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b, 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, + 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098, + 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, + 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc, 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, + 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3, + 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, + 0xbcbcbc, 0xe2e2e2, 0xffffff + ] + self.irc = self.nearest_hex_color(pixel_u32, self.RGB99) + + def nearest_hex_color(self, pixel_u32, hex_colors): + rgb_colors = [hex_to_rgb(color) for color in hex_colors] + rgb_colors.sort(key=lambda rgb: color_distance_squared(hex_to_rgb(pixel_u32), rgb)) + hex_color = rgb_to_hex(rgb_colors[0]) + return hex_colors.index(hex_color) + +class AnsiPixelPair: + def __init__(self, top, bottom): + self.top = top + self.bottom = bottom + +class AnsiImage: + def __init__(self, image): + self.bitmap = [[rgb_to_hex(image.getpixel((x, y))) for x in range(image.size[0])] for y in range(image.size[1])] + if len(self.bitmap) % 2 != 0: + self.bitmap.append([0 for x in range(image.size[0])]) + ansi_bitmap = [[AnsiPixel(y) for y in x] for x in self.bitmap] + ansi_canvas = list() + for two_rows in range(0, len(ansi_bitmap), 2): + top_row = ansi_bitmap[two_rows] + bottom_row = ansi_bitmap[two_rows+1] + ansi_row = list() + for i in range(len(self.bitmap[0])): + top_pixel = top_row[i] + bottom_pixel = bottom_row[i] + pixel_pair = AnsiPixelPair(top_pixel, bottom_pixel) + ansi_row.append(pixel_pair) + ansi_canvas.append(ansi_row) + self.image = image + self.halfblocks = ansi_canvas \ No newline at end of file diff --git a/scroll.py b/scroll.py index 4fb4723..2d96125 100644 --- a/scroll.py +++ b/scroll.py @@ -2,6 +2,7 @@ # Scroll IRC Art Bot - Developed by acidvegas in Python (https://git.acid.vegas/scroll) import asyncio +import io import json import random import re @@ -10,7 +11,7 @@ import time import urllib.request class connection: - server = 'irc.server.com' + server = 'irc.network.com' port = 6697 ipv6 = False ssl = True @@ -75,7 +76,7 @@ class Bot(): self.last = time.time() self.loops = dict() self.playing = False - self.settings = {'flood':1, 'ignore':'big,birds,doc,gorf,hang,nazi,pokemon', 'lines':300, 'msg':0.03, 'results':25, 'paste':True} + self.settings = {'flood':1, 'ignore':'big,birds,doc,gorf,hang,nazi,pokemon', 'lines':500, 'msg':0.03, 'paste':True, 'png_width':80, 'results':25} self.slow = False self.reader = None self.writer = None @@ -231,6 +232,15 @@ class Bot(): for dir in self.db: await self.sendmsg(chan, '[{0}] {1}{2}'.format(color(str(list(self.db).index(dir)+1).zfill(2), pink), dir.ljust(10), color('('+str(len(self.db[dir]))+')', grey))) await asyncio.sleep(self.settings['msg']) + elif args[1] == 'img' and len(args) == 3: + url = args[2] + if url.startswith('https://') or url.startswith('http://'): + content = urllib.request.urlopen(url).read() + ascii = await img2irc.convert(content, int(self.settings['png_width'])) + if ascii: + for line in ascii: + await self.sendmsg(chan, line) + await asyncio.sleep(self.settings['msg']) elif msg == '.ascii list': await self.sendmsg(chan, underline + color('https://raw.githubusercontent.com/ircart/ircart/master/ircart/.list', light_blue)) elif msg == '.ascii random': @@ -275,7 +285,7 @@ class Bot(): setting = args[2] option = args[3] if setting in self.settings: - if setting in ('flood','lines','msg','results'): + if setting in ('flood','lines','msg','png_width','results'): try: option = float(option) self.settings[setting] = option @@ -324,5 +334,8 @@ try: import chardet except ImportError: raise SystemExit('missing required \'chardet\' library (https://pypi.org/project/chardet/)') -else: - asyncio.run(Bot().connect()) \ No newline at end of file +try: + import img2irc +except ImportError: + raise SystemExit('missing required \'img2irc\' file (https://github.com/ircart/scroll/blob/master/img2irc.py)') +asyncio.run(Bot().connect()) \ No newline at end of file